-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: optimize runtime when build (#5082)
* chore: bump version * feat: analyze runtime * feat: support vite alias * fix: vite config for hooks * fix: add hook * feat: optimize mpa build * fix: mpa generator * fix: compatible with disableRuntime * fix: space size * fix: max old space size * chore: changelog and version * chore: version * chore: optimize code * chore: optimize code * fix: optimize code * fix: optimize code
- Loading branch information
Showing
37 changed files
with
562 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import * as path from 'path'; | ||
import * as fs from 'fs-extra'; | ||
import * as glob from 'fast-glob'; | ||
import { init, parse } from 'es-module-lexer'; | ||
import { transform } from 'esbuild'; | ||
import type { Loader } from 'esbuild'; | ||
|
||
type CheckMap = Record<string, boolean>; | ||
type WebpackAlias = Record<string, string>; | ||
type ViteAlias = {find: string | RegExp, replacement: string}[]; | ||
interface Options { | ||
rootDir: string; | ||
parallel?: number; | ||
analyzeRelativeImport?: boolean; | ||
mode?: 'webpack' | 'vite'; | ||
// webpack mode | ||
alias?: WebpackAlias | ViteAlias; | ||
customRuntimeRules?: Record<string, string[]> | ||
} | ||
|
||
const defaultRuntimeRules = { | ||
'build-plugin-ice-request': ['request', 'useRequest'], | ||
'build-plugin-ice-auth': ['useAuth', 'withAuth'], | ||
}; | ||
|
||
export async function globSourceFiles(sourceDir: string): Promise<string[]> { | ||
return await glob('**/*.{js,ts,jsx,tsx}', { | ||
cwd: sourceDir, | ||
ignore: [ | ||
'**/node_modules/**', | ||
'src/apis/**', | ||
'**/__tests__/**', | ||
], | ||
absolute: true, | ||
}); | ||
} | ||
|
||
function addLastSlash(filePath: string) { | ||
return filePath.endsWith('/') ? filePath : `${filePath}/`; | ||
} | ||
|
||
function getWebpackAliasedPath(filePath: string, alias: WebpackAlias): string { | ||
let aliasedPath = filePath; | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const aliasKey of Object.keys(alias || {})) { | ||
const isStrict = aliasKey.endsWith('$'); | ||
const strictKey = isStrict ? aliasKey.slice(0, -1) : aliasKey; | ||
const aliasValue = alias[aliasKey]; | ||
|
||
if (aliasValue.match(/.(j|t)s(x)?$/)) { | ||
if (aliasedPath === strictKey) { | ||
aliasedPath = aliasValue; | ||
break; | ||
} | ||
} else { | ||
// case: { xyz: '/some/dir' }, { xyz$: '/some/dir' } | ||
// import xyz from 'xyz'; // resolved as '/some/dir' | ||
if (aliasedPath === strictKey) { | ||
aliasedPath = aliasValue; | ||
break; | ||
} else if (isStrict) { | ||
// case: { xyz$: '/some/dir' } | ||
// import xyz from 'xyz/file.js'; // resolved as /abc/node_modules/xyz/file.js | ||
// eslint-disable-next-line no-continue | ||
continue; | ||
} | ||
// case: { xyz: '/some/dir' } | ||
// import xyz from 'xyz/file.js'; // resolved as /some/dir/file.js | ||
const slashedKey = addLastSlash(strictKey); | ||
if (aliasedPath.startsWith(slashedKey)) { | ||
aliasedPath = aliasedPath.replace(new RegExp(`^${slashedKey}`), addLastSlash(aliasValue)); | ||
break; | ||
} | ||
} | ||
} | ||
return aliasedPath; | ||
} | ||
|
||
function getViteAliasedPath(filePath: string, alias: ViteAlias): string { | ||
// apply aliases | ||
let aliasedPath = filePath; | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const { find, replacement } of (alias || [])) { | ||
const matches = | ||
typeof find === 'string' ? aliasedPath.startsWith(find) : find.test(aliasedPath); | ||
if (matches) { | ||
aliasedPath = aliasedPath.replace(find, replacement); | ||
break; | ||
} | ||
} | ||
return aliasedPath; | ||
} | ||
|
||
export function getImportPath(importSpecifier: string, importer: string, options: Pick<Options, 'alias'|'rootDir'|'mode'>) { | ||
const { alias, rootDir, mode } = options; | ||
let aliasedPath = mode === 'webpack' | ||
? getWebpackAliasedPath(importSpecifier, alias as WebpackAlias) | ||
: getViteAliasedPath(importSpecifier, alias as ViteAlias); | ||
if (!path.isAbsolute(aliasedPath)) { | ||
try { | ||
// 检测是否可以在 node_modules 下找到依赖,如果可以直接使用该依赖 | ||
aliasedPath = require.resolve(aliasedPath, { paths: [rootDir]}); | ||
} catch (e) { | ||
// ignore errors | ||
aliasedPath = path.resolve(path.dirname(importer), aliasedPath); | ||
} | ||
} | ||
// filter path with node_modules | ||
if (aliasedPath.includes('node_modules')) { | ||
return ''; | ||
} else if (!path.extname(aliasedPath)) { | ||
// get index file of | ||
const basename = path.basename(aliasedPath); | ||
const patterns = [`${basename}.{js,ts,jsx,tsx}`, `${basename}/index.{js,ts,jsx,tsx}`]; | ||
|
||
return glob.sync(patterns, { | ||
cwd: path.dirname(aliasedPath), | ||
absolute: true, | ||
})[0]; | ||
} | ||
return aliasedPath; | ||
} | ||
|
||
export default async function analyzeRuntime(files: string[], options: Options): Promise<CheckMap> { | ||
const { analyzeRelativeImport, rootDir, alias, mode = 'webpack', parallel, customRuntimeRules = {} } = options; | ||
const parallelNum = parallel ?? 10; | ||
const sourceFiles = [...files]; | ||
const checkMap: CheckMap = {}; | ||
const runtimeRules = { ...defaultRuntimeRules, ...customRuntimeRules }; | ||
// init check map | ||
const checkPlugins = Object.keys(runtimeRules); | ||
checkPlugins.forEach((pluginName) => { | ||
checkMap[pluginName] = false; | ||
}); | ||
|
||
async function analyzeFile(filePath: string) { | ||
let source = fs.readFileSync(filePath, 'utf-8'); | ||
const lang = path.extname(filePath).slice(1); | ||
let loader: Loader; | ||
if (lang === 'ts' || lang === 'tsx') { | ||
loader = lang; | ||
} | ||
try { | ||
if (loader) { | ||
// transform content first since es-module-lexer can't handle ts file | ||
source = (await transform(source, { loader })).code; | ||
} | ||
await init; | ||
const imports = parse(source)[0]; | ||
await Promise.all(imports.map((importSpecifier) => { | ||
return (async () => { | ||
const importName = importSpecifier.n; | ||
// filter source code | ||
if (importName === 'ice') { | ||
const importStr = source.substring(importSpecifier.ss, importSpecifier.se); | ||
checkPlugins.forEach((pluginName) => { | ||
const apiList: string[] = runtimeRules[pluginName]; | ||
if (apiList.some((apiName) => importStr.includes(apiName))) { | ||
checkMap[pluginName] = true; | ||
} | ||
}); | ||
} else if (analyzeRelativeImport) { | ||
let importPath = importName; | ||
if (!path.isAbsolute(importPath)) { | ||
importPath = getImportPath(importPath, filePath, { rootDir, alias, mode }); | ||
} | ||
if (importPath && !sourceFiles.includes(importPath) && fs.existsSync(importPath)) { | ||
await analyzeFile(importPath); | ||
} | ||
} | ||
})(); | ||
})); | ||
} catch (err) { | ||
console.log('[ERROR]', `optimize runtime failed when analyze ${filePath}`); | ||
throw err; | ||
} | ||
} | ||
|
||
try { | ||
for ( | ||
let i = 0; | ||
i * parallelNum < sourceFiles.length && checkPlugins.some((pluginName) => checkMap[pluginName] === false); | ||
i++) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await Promise.all(sourceFiles.slice(i * parallelNum, parallelNum).map((filePath) => { | ||
return analyzeFile(filePath); | ||
})); | ||
} | ||
} catch(err) { | ||
// 如果发生错误,兜底启用所有自动检测的运行时插件,防止错误地移除 | ||
checkPlugins.forEach((pluginName) => { | ||
checkMap[pluginName] = true; | ||
}); | ||
return checkMap; | ||
} | ||
|
||
return checkMap; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
packages/build-app-helpers/src/tests/analyzeRuntime.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import path = require('path'); | ||
import analyzeRuntime, { globSourceFiles, getImportPath } from '../analyzeRuntime'; | ||
|
||
describe('get source file', () => { | ||
const rootDir = path.join(__dirname, './fixtures/analyzeRuntime/'); | ||
it('glob js/ts files', async () => { | ||
const files = await globSourceFiles(rootDir); | ||
expect(files.length).toBe(4); | ||
}); | ||
}); | ||
|
||
describe('get aliased path', () => { | ||
const rootDir = path.join(__dirname, './fixtures/analyzeRuntime/'); | ||
it('webpack alias: { ice: \'./runApp\' }', () => { | ||
const aliasedPath = getImportPath('ice', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: { ice: './runApp' }, | ||
mode: 'webpack', | ||
}); | ||
// file not exists throw error | ||
expect(aliasedPath).toBe(undefined); | ||
}); | ||
|
||
it('get relative path with mode webpack', () => { | ||
const aliasedPath = getImportPath('./store', path.join(rootDir, 'page/index.js'), { | ||
rootDir, | ||
alias: {}, | ||
mode: 'webpack', | ||
}); | ||
// file not exists throw error | ||
expect(aliasedPath).toBe(path.join(rootDir, 'page/store.js')); | ||
}); | ||
|
||
it('get relative path with mode vite', () => { | ||
const aliasedPath = getImportPath('./store', path.join(rootDir, 'page/index.js'), { | ||
rootDir, | ||
alias: {}, | ||
mode: 'vite', | ||
}); | ||
// file not exists throw error | ||
expect(aliasedPath).toBe(path.join(rootDir, 'page/store.js')); | ||
}); | ||
|
||
it('webpack alias: { ice: \'react\' }', () => { | ||
const aliasedPath = getImportPath('ice', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: { ice: 'react' }, | ||
mode: 'webpack', | ||
}); | ||
expect(aliasedPath).toBe(''); | ||
}); | ||
|
||
it('webpack alias: { @: \'rootDir\' }', () => { | ||
const aliasedPath = getImportPath('@/page', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: { '@': rootDir }, | ||
mode: 'webpack', | ||
}); | ||
expect(aliasedPath).toBe(path.join(rootDir, 'page/index.js')); | ||
}); | ||
|
||
it('webpack alias: { @$: \'rootDir\' }', () => { | ||
const aliasedPath = getImportPath('@/page', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: { '@$': rootDir }, | ||
mode: 'webpack', | ||
}); | ||
// without match any alias rule, throw error | ||
expect(aliasedPath).toBe(undefined); | ||
}); | ||
|
||
it('vite alias: [{find: \'ice\', replacement: \'react\'}]', () => { | ||
const aliasedPath = getImportPath('ice', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: [{ find: 'ice', replacement: 'react' }], | ||
mode: 'vite', | ||
}); | ||
// filter node_modules dependencies | ||
expect(aliasedPath).toBe(''); | ||
}); | ||
|
||
it('vite alias: [{find: \'@\', replacement: \'rootDir\'}]', () => { | ||
const aliasedPath = getImportPath('@/page', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: [{ find: '@', replacement: rootDir }], | ||
mode: 'vite', | ||
}); | ||
expect(aliasedPath).toBe(path.join(rootDir, 'page/index.js')); | ||
}); | ||
|
||
it('vite alias: [{find: \/@$\/, replacement: \'rootDir\'}]', () => { | ||
const aliasedPath = getImportPath('@/page', path.join(rootDir, 'index.js'), { | ||
rootDir, | ||
alias: [{ find: /@$/, replacement: rootDir }], | ||
mode: 'vite', | ||
}); | ||
// without match any alias rule, throw error | ||
expect(aliasedPath).toBe(undefined); | ||
}); | ||
}); | ||
|
||
describe('Analyze Runtime', () => { | ||
const rootDir = path.join(__dirname, './fixtures/analyzeRuntime/'); | ||
const entryFile = path.join(rootDir, 'index.js'); | ||
|
||
it('analyze entry file', async () => { | ||
const checkMap = await analyzeRuntime([entryFile], { rootDir }); | ||
expect(checkMap).toStrictEqual({ | ||
'build-plugin-ice-request': true, | ||
'build-plugin-ice-auth': false, | ||
}); | ||
}); | ||
|
||
it('analyze relative import', async () => { | ||
const checkMap = await analyzeRuntime( | ||
[entryFile], | ||
{ rootDir, analyzeRelativeImport: true, alias: { '@': path.join(rootDir)}, customRuntimeRules: { 'build-plugin-ice-store': ['createStore']} } | ||
); | ||
expect(checkMap).toStrictEqual({ | ||
'build-plugin-ice-request': true, | ||
'build-plugin-ice-auth': true, | ||
'build-plugin-ice-store': true, | ||
}); | ||
}); | ||
}); |
1 change: 1 addition & 0 deletions
1
packages/build-app-helpers/src/tests/fixtures/analyzeRuntime/common.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default function common() {}; |
3 changes: 3 additions & 0 deletions
3
packages/build-app-helpers/src/tests/fixtures/analyzeRuntime/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { useRequest } from 'ice'; | ||
import { page } from '@/page'; | ||
import common from '@/common'; |
4 changes: 4 additions & 0 deletions
4
packages/build-app-helpers/src/tests/fixtures/analyzeRuntime/page/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { useAuth } from 'ice'; | ||
import store from './store'; | ||
|
||
export const page = () => {}; |
2 changes: 2 additions & 0 deletions
2
packages/build-app-helpers/src/tests/fixtures/analyzeRuntime/page/store.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { createStore } from 'ice'; | ||
export default createStore; |
3 changes: 1 addition & 2 deletions
3
packages/build-app-templates/src/templates/common/loadStaticModules.ts.ejs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.