Refactor: use swc plugin to remove code (#6144)

* refactor: use swc plugin to remove code

* chore: update version
This commit is contained in:
ClarkXia 2023-04-18 17:01:37 +08:00 committed by GitHub
parent ce94e05489
commit 5dd3c86ed7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 40 additions and 421 deletions

View File

@ -0,0 +1,6 @@
---
'@ice/webpack-config': patch
'@ice/app': patch
---
refactor: use swc plugin for remove code

View File

@ -0,0 +1,5 @@
---
'@ice/bundles': patch
---
fix: update version of @ice/swc-plugin-keep-export(0.1.4-2)

View File

@ -17,7 +17,7 @@
"dependencies": {
"@swc/core": "1.3.19",
"@ice/swc-plugin-remove-export": "0.1.2",
"@ice/swc-plugin-keep-export": "0.1.4",
"@ice/swc-plugin-keep-export": "0.1.4-2",
"@ice/swc-plugin-node-transform": "0.1.0-5",
"ansi-html-community": "^0.0.8",
"html-entities": "^2.3.2",

View File

@ -36,10 +36,6 @@
"bugs": "https://github.com/alibaba/ice/issues",
"homepage": "https://v3.ice.work",
"dependencies": {
"@babel/generator": "7.18.10",
"@babel/parser": "7.18.10",
"@babel/traverse": "7.18.10",
"@babel/types": "7.18.10",
"@ice/bundles": "0.1.8",
"@ice/route-manifest": "1.1.1",
"@ice/runtime": "^1.1.5",
@ -56,7 +52,6 @@
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.3",
"ejs": "^3.1.6",
"estree-walker": "^3.0.2",
"fast-glob": "^3.2.11",
"find-up": "^5.0.0",
"fs-extra": "^10.0.0",

View File

@ -1,55 +0,0 @@
import * as fs from 'fs';
import type { Plugin } from 'esbuild';
import { parse, type ParserOptions } from '@babel/parser';
import babelTraverse from '@babel/traverse';
import babelGenerate from '@babel/generator';
import removeTopLevelCode from '../utils/babelPluginRemoveCode.js';
import formatPath from '../utils/formatPath.js';
import { logger } from '../utils/logger.js';
// @ts-ignore @babel/traverse is not a valid export in esm
const traverse = babelTraverse.default || babelTraverse;
// @ts-ignore @babel/generate is not a valid export in esm
const generate = babelGenerate.default || babelGenerate;
const removeCodePlugin = (keepExports: string[], transformInclude: (id: string) => boolean): Plugin => {
const parserOptions: ParserOptions = {
sourceType: 'module',
plugins: [
'jsx',
'importMeta',
'topLevelAwait',
'classProperties',
'classPrivateMethods',
],
};
return {
name: 'esbuild-remove-top-level-code',
setup(build) {
build.onLoad({ filter: /\.(js|jsx|ts|tsx)$/ }, async ({ path: id }) => {
if (!transformInclude(formatPath(id))) {
return;
}
const source = await fs.promises.readFile(id, 'utf-8');
let isTS = false;
if (id.match(/\.(ts|tsx)$/)) {
isTS = true;
parserOptions.plugins.push('typescript', 'decorators-legacy');
}
try {
const ast = parse(source, parserOptions);
traverse(ast, removeTopLevelCode(keepExports));
const contents = generate(ast).code;
return {
contents,
loader: isTS ? 'tsx' : 'jsx',
};
} catch (error) {
logger.debug('Remove top level code error.', `\nFile id: ${id}`, `\n${error.stack}`);
}
});
},
};
};
export default removeCodePlugin;

View File

@ -1,7 +1,6 @@
import * as path from 'path';
import fs from 'fs-extra';
import type { ServerCompiler } from '../types/plugin.js';
import removeTopLevelCode from '../esbuild/removeTopLevelCode.js';
import { getCache, setCache } from '../utils/persistentCache.js';
import { getFileHash } from '../utils/hash.js';
import dynamicImport from '../utils/dynamicImport.js';
@ -15,7 +14,7 @@ type GetOutfile = (entry: string, exportNames: string[]) => string;
interface CompileConfig {
entry: string;
rootDir: string;
transformInclude: (id: string) => boolean;
transformInclude?: (id: string) => boolean;
needRecompile?: (entry: string, options: string[]) => Promise<boolean | string>;
getOutfile?: GetOutfile;
}
@ -48,13 +47,19 @@ class Config {
format: 'esm',
outfile,
plugins: [
removeTopLevelCode(keepExports, transformInclude),
// External node builtin modules, such as `fs`, it will be imported by weex document.
externalBuiltinPlugin(),
],
].filter(Boolean),
sourcemap: false,
logLevel: 'silent', // The main server compiler process will log it.
}, {});
}, {
swc: {
keepExports: {
value: keepExports,
include: transformInclude,
},
},
});
if (!error) {
this.status = 'RESOLVED';
return outfile;
@ -197,8 +202,6 @@ export const getRouteExportConfig = (rootDir: string) => {
entry: routeConfigFile,
rootDir,
getOutfile: getRouteConfigOutfile,
// Only remove top level code for route component file.
transformInclude: (id) => id.includes('src/pages'),
needRecompile: async (entry) => {
let cached = false;
try {
@ -218,8 +221,6 @@ export const getRouteExportConfig = (rootDir: string) => {
entry: loadersConfigFile,
rootDir,
getOutfile: getdataLoadersConfigOutfile,
// Only remove top level code for route component file.
transformInclude: (id) => id.includes('src/pages'),
needRecompile: async (entry) => {
let cached = false;
const cachedKey = `loader_config_file_${process.env.__ICE_VERSION__}`;

View File

@ -1,128 +0,0 @@
import type { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
const removeUnreferencedCode = (nodePath: NodePath<t.Program>) => {
let hasRemoved = false;
// Update bindings removed in enter hooks.
nodePath.scope.crawl();
for (const [, binding] of Object.entries(nodePath.scope.bindings)) {
if (!binding.referenced && binding.path.node) {
const nodeType = binding.path.node.type;
if (['VariableDeclarator', 'ImportSpecifier', 'FunctionDeclaration'].includes(nodeType)) {
if (nodeType === 'ImportSpecifier' && (binding.path.parentPath.node as t.ImportDeclaration)?.specifiers.length === 1) {
binding.path.parentPath.remove();
} else if (nodeType === 'VariableDeclarator') {
if (binding.identifier === binding.path.node.id) {
binding.path.remove();
} else {
if (binding.path.node.id.type === 'ArrayPattern') {
binding.path.node.id.elements = binding.path.node.id.elements.filter((element) =>
(element !== binding.identifier && (element as t.RestElement)?.argument !== binding.identifier));
if (binding.path.node.id.elements.length === 0) {
binding.path.remove();
}
} else if (binding.path.node.id.type === 'ObjectPattern') {
binding.path.node.id.properties = binding.path.node.id.properties.filter((property) => {
const { value, key } = property as t.ObjectProperty;
const argument = (property as t.RestElement)?.argument;
return value !== binding.identifier && argument !== binding.identifier &&
(key?.type !== 'Identifier' || (key as t.Identifier)?.name !== binding.identifier.name);
});
if (binding.path.node.id.properties.length === 0) {
binding.path.remove();
}
}
}
} else {
binding.path.remove();
}
hasRemoved = true;
} else if (['ImportDefaultSpecifier'].includes(nodeType)) {
binding.path.parentPath.remove();
hasRemoved = true;
}
}
}
if (hasRemoved) {
// Remove code until there is no more to removed.
removeUnreferencedCode(nodePath);
}
};
const keepExportCode = (identifier: t.Identifier, keepExports: string[]) => {
return keepExports.some((exportString) => {
return t.isIdentifier(identifier, { name: exportString });
});
};
const removeTopLevelCode = (keepExports: string[] = []) => {
return {
ExportNamedDeclaration: {
enter(nodePath: NodePath<t.ExportNamedDeclaration>) {
const { node } = nodePath;
// Exp: export function pageConfig() {}
const isFunctionExport = t.isFunctionDeclaration(node.declaration) &&
keepExportCode(node.declaration.id, keepExports);
// Exp: export const pageConfig = () => {}
const isVariableExport = t.isVariableDeclaration(node.declaration) &&
keepExportCode(node.declaration.declarations![0]?.id as t.Identifier, keepExports);
// Exp: export { pageConfig };
if (node.specifiers && node.specifiers.length > 0) {
nodePath.traverse({
ExportSpecifier(nodePath: NodePath<t.ExportSpecifier>) {
if (!keepExportCode(nodePath.node.exported as t.Identifier, keepExports)) {
nodePath.remove();
}
},
});
node.specifiers = node.specifiers.filter(specifier =>
keepExportCode(specifier.exported as t.Identifier, keepExports));
} else if (!isFunctionExport && !isVariableExport) {
// Remove named export expect defined in keepExports.
nodePath.remove();
}
},
},
ExportDefaultDeclaration: {
enter(nodePath: NodePath<t.ExportDefaultDeclaration>) {
// Remove default export declaration.
if (!keepExports.includes('default')) {
nodePath.remove();
}
},
},
ExpressionStatement: {
enter(nodePath: NodePath<t.ExpressionStatement>) {
// Remove top level call expression.
if (nodePath.parentPath.isProgram()) {
if (t.isCallExpression(nodePath.node.expression) || t.isAssignmentExpression(nodePath.node.expression)) {
nodePath.remove();
}
}
},
},
ImportDeclaration: {
enter(nodePath: NodePath<t.ImportDeclaration>) {
// Remove import statement without specifiers.
if (nodePath.node.specifiers.length === 0) {
nodePath.remove();
}
},
},
'IfStatement|TryStatement|WhileStatement|DoWhileStatement': {
// Remove statement even if it's may cause variable changed.
enter(nodePath: NodePath<t.IfStatement | t.TryStatement | t.WhileStatement>) {
// TODO: check expression statement if it is changed top level variable referenced by pageConfig
nodePath.remove();
},
},
Program: {
exit(nodePath: NodePath<t.Program>) {
removeUnreferencedCode(nodePath);
},
},
};
};
export default removeTopLevelCode;

View File

@ -1,2 +0,0 @@
const a = 1;
export default a;

View File

@ -1,6 +0,0 @@
const pageConfig = () => {};
const getData = () => {};
export {
pageConfig,
getData,
};

View File

@ -1,2 +0,0 @@
export const getData = () => {};
export const pageConfig = () => {};

View File

@ -1,2 +0,0 @@
const a = {};
a.test = 1;

View File

@ -1,3 +0,0 @@
export default function Bar() {}
export function pageConfig() {}
export function getData() {}

View File

@ -1,4 +0,0 @@
let a = 1;
if (true) {
a = 2;
}

View File

@ -1,5 +0,0 @@
function a() {}
a();
console.log('test', window.a);
const b = [];
b.map(() => {});

View File

@ -1,8 +0,0 @@
import { a, b } from 'test';
import { a as c } from 'test-a';
import d from 'test-d';
import 'test-c';
export function pageConfig() {
return { a: 1 };
}

View File

@ -1,11 +0,0 @@
import React, { useState, useEffect } from 'react';
export default function Bar() {
const [str] = useState('');
useEffect(() => {}, []);
return <React.Fragment>{str}</React.Fragment>;
}
export function pageConfig() {
return { a: 1 };
}

View File

@ -1,7 +0,0 @@
const c = {};
const { a = '', b = '' } = c;
const d = () => {
console.log(a, b);
}
export default d;

View File

@ -1,5 +0,0 @@
import { a, b } from 'test';
function test() {
a();
}

View File

@ -1,17 +0,0 @@
import { a, z } from 'a';
import b from 'b';
import c from 'c';
import d from 'd';
const [e, f, ...rest] = a;
const { h, j } = b;
const [x, ...m] = c;
const zz = 'x';
const { k, l, ...s } = d;
export function pageConfig() {
return {
x,
k,
};
}

View File

@ -1,8 +0,0 @@
let j = 2;
let i = 2;
while (j < 3) {
j++;
}
do {
i++;
} while (i < 5);

View File

@ -1,108 +0,0 @@
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { expect, it, describe } from 'vitest';
import { parse, type ParserOptions } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import removeTopLevelCodePlugin from '../src/utils/babelPluginRemoveCode';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const parserOptions: ParserOptions = {
sourceType: 'module',
plugins: [
'jsx',
'importMeta',
'topLevelAwait',
'classProperties',
'classPrivateMethods',
'typescript',
'decorators-legacy',
],
};
describe('remove top level code', () => {
it('remove specifier export', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/export-specifier.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('const pageConfig = () => {};export { pageConfig };');
});
it('remove variable export', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/export-variable.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export const pageConfig = () => {};');
});
it('remove function export', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/function-exports.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function pageConfig() {}');
});
it('remove if statement', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/if.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content).toBe('');
});
it('remove import statement', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/import.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function pageConfig() { return { a: 1 };}');
});
it('remove mixed import statement', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/mixed-import.tsx'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function pageConfig() { return { a: 1 };}');
});
it('remove IIFE code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/iife.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content).toBe('');
});
it('remove loop code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/while.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content).toBe('');
});
it('remove nested reference code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/reference.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content).toBe('');
});
it('remove variable declaration code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/vars.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('import c from \'c\';import d from \'d\';const [x] = c;const { k} = d;export function pageConfig() { return { x, k };}');
});
it('keep export default', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/export-default.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['default']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('const a = 1;export default a;');
});
it('remove expression statement', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/expression.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['default']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('');
});
it('remove nested reference code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/properties.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['pageConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('');
});
});

View File

@ -39,7 +39,7 @@ type Experimental = Configuration['experiments'];
interface SwcOptions {
removeExportExprs?: string[];
compilationConfig?: SwcCompilationConfig | ((source: string, id: string) => SwcCompilationConfig);
keepExports?: string[];
keepExports?: string[] | { value: string[]; include?: (id: string) => boolean };
nodeTransform?: boolean;
}

View File

@ -114,23 +114,22 @@ const compilationPlugin = (options: Options): UnpluginOptions => {
]);
}
}
if (keepExports) {
if (isRouteEntry(id)) {
swcPlugins.push([
swcPluginKeepExport,
keepExports,
]);
} else if (isAppEntry(id)) {
let keepList;
if (keepExports.indexOf('pageConfig') > -1) {
// when build for pageConfig, should keep default, it equals to getAppConfig
keepList = keepExports.concat(['default']);
const keepList = Array.isArray(keepExports) ? keepExports : keepExports.value;
const customInlcude = !Array.isArray(keepExports) && keepExports?.include;
let matchRule = false;
if (customInlcude) {
matchRule = customInlcude(id);
} else {
keepList = keepExports;
const matchRoute = isRouteEntry(id);
const matchEntry = isAppEntry(id);
if (matchEntry && keepList.indexOf('pageConfig') > -1) {
// when build for pageConfig, should keep default, it equals to getAppConfig
keepList.push('default');
}
matchRule = matchRoute || matchEntry;
}
if (matchRule) {
swcPlugins.push([
swcPluginKeepExport,
keepList,

View File

@ -800,7 +800,7 @@ importers:
packages/bundles:
specifiers:
'@ice/swc-plugin-keep-export': 0.1.4
'@ice/swc-plugin-keep-export': 0.1.4-2
'@ice/swc-plugin-node-transform': 0.1.0-5
'@ice/swc-plugin-remove-export': 0.1.2
'@pmmmwh/react-refresh-webpack-plugin': 0.5.10
@ -879,7 +879,7 @@ importers:
webpack-dev-server: 4.11.1
ws: ^8.4.2
dependencies:
'@ice/swc-plugin-keep-export': 0.1.4
'@ice/swc-plugin-keep-export': 0.1.4-2
'@ice/swc-plugin-node-transform': 0.1.0-5
'@ice/swc-plugin-remove-export': 0.1.2
'@swc/core': 1.3.19
@ -980,10 +980,6 @@ importers:
packages/ice:
specifiers:
'@babel/generator': 7.18.10
'@babel/parser': 7.18.10
'@babel/traverse': 7.18.10
'@babel/types': 7.18.10
'@ice/bundles': 0.1.8
'@ice/route-manifest': 1.1.1
'@ice/runtime': ^1.1.5
@ -1010,7 +1006,6 @@ importers:
dotenv-expand: ^8.0.3
ejs: ^3.1.6
esbuild: ^0.16.5
estree-walker: ^3.0.2
fast-glob: ^3.2.11
find-up: ^5.0.0
fs-extra: ^10.0.0
@ -1032,10 +1027,6 @@ importers:
webpack-dev-server: ^4.7.4
yargs-parser: ^21.1.1
dependencies:
'@babel/generator': 7.18.10
'@babel/parser': 7.18.10
'@babel/traverse': 7.18.10
'@babel/types': 7.18.10
'@ice/bundles': link:../bundles
'@ice/route-manifest': link:../route-manifest
'@ice/runtime': link:../runtime
@ -1052,7 +1043,6 @@ importers:
dotenv: 16.0.3
dotenv-expand: 8.0.3
ejs: 3.1.8
estree-walker: 3.0.3
fast-glob: 3.2.12
find-up: 5.0.0
fs-extra: 10.1.0
@ -5187,8 +5177,8 @@ packages:
- react-native
dev: false
/@ice/swc-plugin-keep-export/0.1.4:
resolution: {integrity: sha512-fOc09KALmL2zJK1xNGTEt/C27mXL7NVn/v1eRjjuM4uer+qmWIxYXIa9dpfTX5ZUn8zXhrKH8lGdczoKHCzyQQ==}
/@ice/swc-plugin-keep-export/0.1.4-2:
resolution: {integrity: sha512-kmQms1GTc4LBfPK+SyEo3UBBX0GMhPB02VJPA34AtIpNmaWApgmkqQBHbmeeV8ad0nrHrxTfRL80ldMhplyC4g==}
dev: false
/@ice/swc-plugin-node-transform/0.1.0-5:
@ -11130,12 +11120,6 @@ packages:
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
/estree-walker/3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
dependencies:
'@types/estree': 1.0.0
dev: false
/esutils/2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}