diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index 72009746..48567c96 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -146,6 +146,13 @@ module.exports = { ### optimization.allowEmptyAttributes 是否允许空属性,默认 `true`,不要改动该配置,除非你清楚自己要做什么:warning:。 +## treeShake + +- **类型**: `Boolean` default: `false` +- **用法**: + +是否开启 `tree-shake` 。只有为 `true` 且非 `watch` 模式下 `tree-shake` 才会生效。 + ## plugins 目前支持的插件有: diff --git a/package-lock.json b/package-lock.json index 39ffa5c1..0a6689c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12136,9 +12136,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.21.tgz?cache=0&sync_timestamp=1613835838133&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.21.tgz", + "integrity": "sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw=" }, "lodash._reinterpolate": { "version": "3.0.0", diff --git a/package.json b/package.json index 1931445b..b080ba25 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "@vuepress/theme-default": "^1.5.4", + "lodash": "^4.17.21", "vue-server-renderer": "^2.6.12" } } diff --git a/packages/wxa-cli/.eslintrc.json b/packages/wxa-cli/.eslintrc.json index e5b78f4a..7d1e7515 100644 --- a/packages/wxa-cli/.eslintrc.json +++ b/packages/wxa-cli/.eslintrc.json @@ -27,7 +27,8 @@ "no-trailing-spaces": "off", "require-jsdoc": "off", "camelcase": "warn", - "no-invalid-this": "warn" + "no-invalid-this": "warn", + "linebreak-style": ["off", "windows"] }, "plugins": [] } diff --git a/packages/wxa-cli/package-lock.json b/packages/wxa-cli/package-lock.json index df92d531..f90277a4 100644 --- a/packages/wxa-cli/package-lock.json +++ b/packages/wxa-cli/package-lock.json @@ -1738,9 +1738,9 @@ } }, "@wxa/compiler-babel": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@wxa/compiler-babel/-/compiler-babel-2.2.0.tgz", - "integrity": "sha512-kjQL/T0vLdulAoaCVzlmAE3UbtC1e/c6JVmf/OL9hi7oDspAhlyHSySOH9JhTGYzOKPYbJpBrO1EoMMsnLWRMA==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@wxa/compiler-babel/-/compiler-babel-2.2.7.tgz", + "integrity": "sha512-BX+Mft8cXGnarS1CgxhrujW4yHnnyQvKAm+l7D9CI3da4UwYkqzA/2XicQG0Zu5gIrekvvosUp8zRE9truZepA==", "requires": { "@babel/core": "^7.1.0", "debug": "^4.0.1", @@ -1750,11 +1750,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "find-cache-dir": { @@ -1770,11 +1770,11 @@ } }, "@wxa/compiler-sass": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@wxa/compiler-sass/-/compiler-sass-2.2.0.tgz", - "integrity": "sha512-te92dBDbVQDMaqaTopmK9pIx8W2WWg3bESVv4T71WS4UHLzy3boqnjICS5w4ZfCHpeL4imvMiAVZe37tHcT3Jw==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@wxa/compiler-sass/-/compiler-sass-2.2.7.tgz", + "integrity": "sha512-bQfqxJbTFrfWN5uVSR/ajLMQ/olyKAMvGBYUHcmZMUDl1zYzi1ldM3d7CrDFMaaPgdsJMqUV0fSVQGDSzyWjoQ==", "requires": { - "node-sass": "^4.12.0" + "node-sass": "^4.14.1" } }, "@xtuc/ieee754": { @@ -2031,9 +2031,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "babel-code-frame": { "version": "6.26.0", @@ -4210,9 +4210,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "htmlparser2": { "version": "3.10.1", @@ -4762,9 +4762,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "loose-envify": { "version": "1.4.0", @@ -4978,16 +4978,16 @@ } }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", "requires": { - "mime-db": "1.44.0" + "mime-db": "1.49.0" } }, "mimic-fn": { @@ -6518,9 +6518,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==" }, "split-string": { "version": "3.1.0", diff --git a/packages/wxa-cli/package.json b/packages/wxa-cli/package.json index f25ccce0..22e57d86 100644 --- a/packages/wxa-cli/package.json +++ b/packages/wxa-cli/package.json @@ -69,7 +69,8 @@ "promise.prototype.finally": "^3.1.0", "shelljs": "^0.8.3", "tapable": "^1.0.0", - "valid-url": "^1.0.9" + "valid-url": "^1.0.9", + "lodash": "^4.17.21" }, "publishConfig": { "access": "public" diff --git a/packages/wxa-cli/src/const/wxaConfigs.js b/packages/wxa-cli/src/const/wxaConfigs.js index 4e6b28d7..504e03ce 100644 --- a/packages/wxa-cli/src/const/wxaConfigs.js +++ b/packages/wxa-cli/src/const/wxaConfigs.js @@ -43,6 +43,7 @@ export default class DefaultWxaConfigs { allowEmptyAttributes: true, transformPxToRpx: false, }, + treeShake: false, }; } } diff --git a/packages/wxa-cli/src/schedule.js b/packages/wxa-cli/src/schedule.js index f31ab752..a0ed5fcb 100644 --- a/packages/wxa-cli/src/schedule.js +++ b/packages/wxa-cli/src/schedule.js @@ -17,6 +17,7 @@ import DependencyResolver from './helpers/dependencyResolver'; import ProgressTextBar from './helpers/progressTextBar'; import Preformance from './helpers/performance'; import simplify from './helpers/simplifyObj'; +import {treeShake} from './tree-shake/index'; let debug = debugPKG('WXA:Schedule'); @@ -72,6 +73,7 @@ class Schedule { // if is mounting compiler, all task will be blocked. this.$isMountingCompiler = false; this.progress = new ProgressTextBar(this.current, wxaConfigs); + this.$depJsPending = []; // save all app configurations for compile time. // such as global components. @@ -120,17 +122,28 @@ class Schedule { async $doDPA() { let tasks = []; + while (this.$depPending.length) { let dep = this.$depPending.shift(); + if (this.wxaConfigs.treeShake && !this.cmdOptions.watch && dep.src.endsWith('.js')) { + this.$depJsPending.push(dep); + } else { + tasks.push(this.$parse(dep)); + } + // debug('file to parse %O', dep); - tasks.push(this.$parse(dep)); } let succ = await Promise.all(tasks); if (this.$depPending.length === 0) { // dependencies resolve complete + if (this.wxaConfigs.treeShake && !this.cmdOptions.watch) { + let sub = await this.$dojsDPA(); + return succ.concat(sub); + } + this.progress.clean(); return succ; } else { @@ -139,6 +152,53 @@ class Schedule { } } + async $dojsDPA() { + this.$depPending = this.$depJsPending; + this.$depJsPending = []; + let entries = this.$depPending.map((item)=>({src: item.src, content: item.content})); + console.time('shake'); + let contents = treeShake( + { + entry: entries, + resolveSrc: { + root: 'src', + alias: this.wxaConfigs.resolve.alias, + }, + commonJS: { + enable: true, + }, + } + ); + console.timeEnd('shake'); + + let run = async () => { + let tasks = []; + + while (this.$depPending.length) { + let dep = this.$depPending.shift(); + let content = contents[dep.src] && contents[dep.src].formattedCode; + if (content) { + dep.content = content; + } + + tasks.push(this.$parse(dep)); + } + + let succ = await Promise.all(tasks); + + if (this.$depPending.length === 0) { + // dependencies resolve complete + this.progress.clean(); + return succ; + } else { + let sub = await run(); + return succ.concat(sub); + } + }; + + return run(); + } + async $parse(dep) { if (dep.color === COLOR.COMPILED) return dep; if (dep.color === COLOR.CHANGED) dep.code = void(0); diff --git a/packages/wxa-cli/src/tree-shake/config.js b/packages/wxa-cli/src/tree-shake/config.js new file mode 100644 index 00000000..8c3cb25a --- /dev/null +++ b/packages/wxa-cli/src/tree-shake/config.js @@ -0,0 +1,39 @@ +module.exports = { + // { + // src: '', // 路径 + // content: '', // 文件内容(可选) + // }, + entry: [], + resolveSrc: { + // '/a/b/c',绝对路径根目录 + // 例如src, '/a/b/c' 转换为 /src/a/b/b + root: 'src', + // {'@': 'src'},路径别名 + alias: {}, + npm: 'node_modules', + }, + commonJS: { + enable: false, + // 无法追踪动态引入的模块 + // 如果有模块被动态引入,需要在这里设置该模块文件路径 + // 将跳过对该文件的 tree shake + dynamicRequireTargets: [], + // 设置 exports 上的哪些属性不会被转换为 esm + // 默认值有 '__esModule' + ingoreKeys: ['__esModule'], + }, + parseOptions: { + plugins: [ + ['decorators', {decoratorsBeforeExport: true}], + 'classProperties', + 'exportNamespaceFrom', + 'exportDefaultFrom', + 'objectRestSpread', + ], + sourceType: 'unambiguous', + }, + generateOptions: { + decoratorsBeforeExport: true, + }, + debug: false, +}; diff --git a/packages/wxa-cli/src/tree-shake/graph.js b/packages/wxa-cli/src/tree-shake/graph.js new file mode 100644 index 00000000..55769847 --- /dev/null +++ b/packages/wxa-cli/src/tree-shake/graph.js @@ -0,0 +1,386 @@ +const traverse = require('@babel/traverse').default; +const t = require('@babel/types'); +const { + readFile, + resolveDepSrc, + parseESCode, + dceDeclaration, + log, +} = require('./util'); +let config = require('./config'); +let {CommonJS} = require('./shake-cjs'); + +class Graph { + constructor(entries) { + this.entries = config.entry; + this.analysis(); + } + + getAbsolutePath(fileSrc, depSrc) { + let s = resolveDepSrc({ + fileSrc, + depSrc, + ...config.resolveSrc, + }); + + if (!s.endsWith('.js')) { + s += '.js'; + } + + return s; + } + + getExpSrc(node, src) { + let expSrc = ''; + + if (node.source) { + expSrc = this.getAbsolutePath(src, node.source.value); + } else { + expSrc = src; + } + + return expSrc; + } + + collectImport(src) { + let imports = {}; + let store = (name, path, node) => { + let impSrc = this.getAbsolutePath(src, node.source.value); + imports[impSrc] = imports[impSrc] || {}; + if (name === '' || path === '') { + return; + } + imports[impSrc][name] = path; + }; + + let visitor = { + ImportDeclaration: { + enter: (path) => { + let {node} = path; + let specifierPaths = path.get('specifiers'); + if (specifierPaths && specifierPaths.length) { + specifierPaths.forEach((specifierPath) => { + let specifierNode = specifierPath.node; + let name = + specifierNode.imported && + specifierNode.imported.name; + + if (!name) { + if ( + specifierNode.type === + 'ImportDefaultSpecifier' + ) { + name = 'default'; + } else if ( + specifierNode.type === + 'ImportNamespaceSpecifier' + ) { + name = '*'; + } + } + store(name, specifierPath, node); + }); + } else { + // import './a' + store('', '', node); + } + }, + }, + }; + + return {visitor, imports}; + } + + collectExport(src, isRoot) { + let exports = {}; + + let store = (name, path, node) => { + if (isRoot) { + path.$extReferences = new Set().add('root'); + } else { + path.$extReferences = new Set(); + } + let expSrc = this.getExpSrc(node, src); + exports[expSrc] = exports[expSrc] || {}; + exports[expSrc][name] = path; + }; + + let storeSpecifiers = (path, node) => { + let specifierPaths = path.get('specifiers'); + specifierPaths.forEach((specifierPath) => { + let name = specifierPath.node.exported.name; + store(name, specifierPath, node); + }); + }; + + let transformExportDeclarationToSpecifiers = (path) => { + let declarationPath = path.get('declaration'); + let declarationNode = declarationPath.node; + let specifiers = []; + + if (declarationNode.type === 'FunctionDeclaration') { + let name = declarationNode.id.name; + specifiers.push( + t.exportSpecifier(t.identifier(name), t.identifier(name)) + ); + } else if (declarationNode.type === 'VariableDeclaration') { + let declarationPaths = declarationPath.get('declarations'); + declarationPaths.forEach((variableDeclaratorPath) => { + let name = variableDeclaratorPath.node.id.name; + specifiers.push( + t.exportSpecifier( + t.identifier(name), + t.identifier(name) + ) + ); + }); + } else if (declarationNode.type === 'ClassDeclaration') { + let name = declarationNode.id.name; + specifiers.push( + t.exportSpecifier(t.identifier(name), t.identifier(name)) + ); + } + + return {specifiers, declarationPath}; + }; + + let visitor = { + ExportNamedDeclaration: { + enter: (path) => { + let {node} = path; + + if (node.specifiers && node.specifiers.length) { + storeSpecifiers(path, node); + } else { + // 类似于export function mm(){} + // 单独声明 function mm(){},并export default mm + let { + specifiers, + declarationPath, + } = transformExportDeclarationToSpecifiers(path); + + path.insertBefore(declarationPath.node); + + let exportNamedDeclaration = t.exportNamedDeclaration( + null, + specifiers + ); + let newExportPath = path.insertAfter( + exportNamedDeclaration + )[0]; + + path.remove(); + storeSpecifiers(newExportPath, node); + } + }, + }, + ExportDefaultDeclaration: { + enter: (path) => { + let {node} = path; + let declarationPath = path.get('declaration'); + let declarationNode = declarationPath.node; + + // 类似于 export default function mm(){} + // 单独声明 mm,然后 export default mm + if ( + (t.isFunctionDeclaration(declarationNode) || + t.isClassDeclaration(declarationNode)) && + declarationNode.id && + declarationNode.id.name + ) { + path.insertBefore(declarationNode); + declarationPath.replaceWith(declarationNode.id); + } else if ( + !t.isFunctionDeclaration(declarationNode) && + !t.isClassDeclaration(declarationNode) && + !t.isIdentifier(declarationNode) + ) { + // 类似于 export default {} + // 单独声明 let _temp = {},然后 export default _temp + let id = path.scope.generateUidIdentifier(); + let declaration = t.variableDeclaration('let', [ + t.variableDeclarator(id, declarationNode), + ]); + path.insertBefore(declaration); + declarationPath.replaceWith(id); + } + + // 剩余情况:export default a, export default function(){} + + store('default', path, node); + }, + }, + ExportAllDeclaration: { + enter: (path) => { + let {node} = path; + store('*', path, node); + }, + }, + }; + + return {visitor, exports}; + } + + dceDeclaration(ast) { + let currentScope = null; + traverse(ast, { + enter: (path) => { + let scope = path.scope; + if (currentScope !== scope) { + currentScope = scope; + dceDeclaration(currentScope); + } + }, + }); + } + + analysis() { + let analyzedFile = {}; + + let doAnalysis = (entry, isRoot) => { + let src = ''; + let content = ''; + + if (typeof entry === 'string') { + src = entry; + } else { + src = entry.src; + content = entry.content; + } + + if (analyzedFile[src]) { + return analyzedFile[src]; + } + + log('graph', src); + + let code = content || readFile(src); + let ast = parseESCode(code); + + this.dceDeclaration(ast); + + let topScope = null; + + let commonjs = new CommonJS({src, code, ast}); + + let {visitor: exportVisitor, exports} = this.collectExport( + src, + isRoot + ); + let {visitor: importVisitor, imports} = this.collectImport(src); + + traverse(ast, { + Program: { + enter: (path) => { + topScope = path.scope; + }, + }, + ...exportVisitor, + ...importVisitor, + }); + + // import * 和 export * 不包括default + // export * from '' 和 export 本文件冲突,export 本文件优先级更高 + // export * from '' 互相冲突,后export * from '' 优先 + // export {} from '', 从其他文件导出,导出的变量无法在本文件使用 + // export default function(){},导出的函数没有name时,不能在本文件使用 + // 不存在语法 export default let a =1 + /** + * imports: { + * [路径]: { + * [name]: ImportSpecifier, + * default: ImportDefaultSpecifier, + * *: ImportNamespaceSpecifier + * } + * } + * + * exports: { + * [本文件路径]: { + * // export default function(){} | export default{} | export {a as default} | export default a + * default: ExportDefaultDeclaration, + * // export {a as aaa,b,c} + * [name]: ExportSpecifier + * }, + * [其他路径]: { + * // export {a as aaa,b,c} from '' | export * as all from '' + * [name]: ExportSpecifier + * // export {default} from '' | export {a as default} from '' | export * as default from '' + * default: ExportSpecifier, + * // export * from '' + * *: ExportAllDeclaration + * }, + * } + */ + + commonjs.state.cjsRequireModules.forEach( + (names, requireSrc) => { + let abSrc = this.getAbsolutePath(src, requireSrc); + + imports[abSrc] = imports[abSrc] || {}; + Array.from(names).forEach((name) => { + if (name === 'default') { + imports[abSrc].default = 'custom_import_name'; + imports[abSrc]['*'] = 'custom_import_name'; + return; + } + + imports[abSrc][name] = 'custom_import_name'; + }); + } + ); + + if (!commonjs.state.dynamicRequired) { + commonjs.state.cjsExportModules.forEach( + (path, name) => { + exports[src] = exports[src] || {}; + exports[src][name] = path; + path.$isCjsExport = true; + + if (isRoot) { + path.$extReferences = new Set().add('root'); + } else { + path.$extReferences = new Set(); + } + + // log('cjsExportName', name, path.toString()); + } + ); + } + + let dep = { + src, + code, + ast, + imports, + exports, + children: [], + topScope, + isRoot, + commonjs, + }; + + analyzedFile[src] = dep; + + Object.keys(dep.imports).forEach((childSrc, index) => { + dep.children.push(doAnalysis(childSrc)); + }); + + // export {} from './a' a 文件也是子节点 + Object.keys(dep.exports).forEach((childSrc) => { + if (childSrc !== src) { + dep.children.push(doAnalysis(childSrc)); + } + }); + + return dep; + }; + + this.roots = this.entries.map((entry) => { + return doAnalysis(entry, true); + }); + } +} + +module.exports = { + Graph, +}; diff --git a/packages/wxa-cli/src/tree-shake/index.js b/packages/wxa-cli/src/tree-shake/index.js new file mode 100644 index 00000000..acf7c7f7 --- /dev/null +++ b/packages/wxa-cli/src/tree-shake/index.js @@ -0,0 +1,34 @@ +let config = require('./config'); +let {Graph} = require('./graph'); +let {TreeShake} = require('./tree-shake'); +let mergeWith = require('lodash/mergeWith'); +let isArray = require('lodash/isArray'); + +function setConfig(options) { + if ( + !options.entry || + !Array.isArray(options.entry) || + !options.entry.length + ) { + throw new Error('Options entry is required'); + } + + mergeWith(config, options, (objValue, srcValue) => { + if (isArray(objValue)) { + return objValue.concat(srcValue); + } + }); +} + +function treeShake(options = {}) { + setConfig(options); + + let graph = new Graph(); + let treeShake = new TreeShake(graph.roots); + let files = treeShake.run(); + return files; +} + +module.exports = { + treeShake, +}; diff --git a/packages/wxa-cli/src/tree-shake/shake-cjs.js b/packages/wxa-cli/src/tree-shake/shake-cjs.js new file mode 100644 index 00000000..5d7e2eae --- /dev/null +++ b/packages/wxa-cli/src/tree-shake/shake-cjs.js @@ -0,0 +1,529 @@ +const traverse = require('@babel/traverse').default; +const {parseESCode, unique, log} = require('./util'); +let t = require('@babel/types'); +const config = require('./config'); + +function getStaticValue(node) { + if (t.isStringLiteral(node) || t.isNumericLiteral(node)) { + return node.value; + } else if ( + t.isTemplateLiteral(node) && + !node.arguments[0].expressions.length + ) { + return node.arguments[0].quasis[0].value.raw; + } + + return false; +} + +// 获得 MemberExpression 节点的属性的静态值 +function getStaticMemberPro(node) { + if (node.computed) { + return getStaticValue(node.property); + } + + return node.property.name; +} + +class CommonJS { + state = { + // { + // moduleName: exports Path + // } + cjsExportModules: new Map(), + // { + // src: [moduleName] + // } + cjsRequireModules: new Map(), + renamed: new Map(), + identifiers: new Set(), + isCJS: false, + isESM: false, + deletedNodes: new Map(), + usedExports: new Set(), + isDynamicUsedExportsProperty: false, + isUsedExportsObject: false, + dynamicRequired: false, + }; + + constructor({src, code, ast}) { + this.src = src; + this.code = code; + this.ast = ast || parseESCode(code); + + let dynamicRequireTargets = config.commonJS.dynamicRequireTargets || []; + + if (!config.commonJS.enable) { + return; + } + + this.ingorekeys = config.commonJS.ingoreKeys; + + this.transform(this.ast); + + if (dynamicRequireTargets.includes(src)) { + this.state.dynamicRequired = true; + return; + } + + if (this.state.isCJS) { + log('---------'); + log('cjsRequireModules', this.state.cjsRequireModules); + log('usedExports', this.state.usedExports); + log( + 'isDynamicUsedExportsProperty', + this.state.isDynamicUsedExportsProperty + ); + log('isUsedExportsObject', this.state.isUsedExportsObject); + // log('cjsExportModules', this.state.cjsExportModules); + log('---------'); + } + } + + reset() { + this.state.deletedNodes.forEach((path, node) => { + if (t.isProgram(path)) { + path.node.body.push(node); + } else { + path.insertBefore(node); + } + }); + } + + deleteCJSExport(exportName) { + if ( + this.state.isDynamicUsedExportsProperty || + this.state.usedExports.has(exportName) || + this.state.isUsedExportsObject + ) { + return; + } + + Array.from(this.state.cjsExportModules).some(([name, cjsPath]) => { + if (exportName === name) { + cjsPath.remove(); + return true; + } + }); + } + + transform(ast) { + let that = this; + traverse(ast, { + CallExpression: { + enter: (path) => { + const {node} = path; + + if ( + t.isIdentifier(node.callee) && + node.callee.name === 'require' + ) { + this.state.isCJS = true; + + let source = getStaticValue(node.arguments[0]); + + // 动态导入 + if (source === false) { + console.warn( + `Dynamic cjsRequireModules are not currently supported: ${path.toString()}. please configure dynamicrequiretargets` + ); + + return; + } + + let {parentPath} = path; + let {node: parentNode} = parentPath; + + let requireNames = this.state.cjsRequireModules.get( + source + ); + if (!requireNames) { + requireNames = new Set(); + this.state.cjsRequireModules.set( + source, + requireNames + ); + } + + // 节点:let {a} = require('a') + // 可能存在的问题,require 的对象被其他函数包装后。例如: + // let {a} = fn(require('a')) + // 此时的 a 并不是原对象的 a 属性 + let objectPatternPath = path.findParent((parent) => { + return t.isObjectPattern(parent.node.id); + }); + + if (objectPatternPath) { + objectPatternPath.node.properties.forEach( + (prop) => { + requireNames.add(prop.key); + } + ); + } else if (t.isMemberExpression(parentNode)) { + // require('a').a + let name = getStaticMemberPro(parentNode); + + // let a = require('a')[a],属于动态导入 + if (name === false) { + console.warn( + `Dynamic cjsRequireModules are not currently supported: ${path.toString()}. please configure dynamicrequiretargets` + ); + + return; + } + + requireNames.add(name); + } else if (source) { + // 节点是 require('./a') 的情况 + + // 声明语句:let a = require('./a') + let declaratorParentPath = path.find((path) => { + return t.isVariableDeclarator(path); + }); + let name = + (declaratorParentPath && + declaratorParentPath.node.id && + declaratorParentPath.node.id.name) || + ''; + let usedNames = []; + + // 只有是声明语句才去分析属性的使用情况 + // 例如:let a = require('./a'),去分析 a 上哪些属性被访问 + if (name) { + let binding = path.scope.getBinding(name); + + binding.referencePaths.every((rPath) => { + let {parent} = rPath; + + if (!t.isMemberExpression(parent)) { + usedNames = []; + return; + } else { + let proKey = getStaticMemberPro(parent); + + if (proKey === false) { + usedNames = []; + return; + } + + usedNames.push(proKey); + return true; + } + }); + + usedNames = unique(usedNames); + usedNames = usedNames.filter( + (n) => n !== 'default' + ); + } + + if (usedNames.length) { + usedNames.forEach((n) => { + requireNames.add(n); + }); + } else { + requireNames.add('default'); + } + } + } + }, + }, + + ModuleDeclaration: { + enter: () => { + this.state.isESM = true; + }, + }, + + AssignmentExpression: { + enter: (path) => { + let generateExportNode = (path, name) => { + let exportName = name; + let rightNode = path.node.right; + + if (t.isIdentifier(rightNode)) { + this.state.cjsExportModules.set( + exportName, + path.find((path) => { + return t.isProgram(path.parentPath); + }) + ); + } else { + let id = path.scope.generateUidIdentifier( + exportName + ); + let declaration = t.variableDeclaration('let', [ + t.variableDeclarator(id, rightNode), + ]); + + path.insertBefore(declaration); + + let rightPath = path.get('right'); + rightPath.replaceWith(id); + + this.state.cjsExportModules.set( + exportName, + path.find((path) => { + return t.isProgram(path.parentPath); + }) + ); + } + }; + + // Check for module.exports. + // 只处理顶级作用域 + // 只处理纯粹的 exports.a=1 语句 + // 即不嵌套在任何其他语句中 + if ( + t.isMemberExpression(path.node.left) && + t.isProgram(path.scope.path) && + t.isProgram(path.parentPath.parentPath) + ) { + const moduleBinding = path.scope.getBinding('module'); + const exportsBinding = path.scope.getBinding('exports'); + + // 节点 module.exports.x = 1; + // 不包含访问子属性 module.exports.x.y = 1; + if ( + t.isMemberExpression(path.node.left.object) && + path.node.left.object.object.name === 'module' + ) { + if (moduleBinding) { + return; + } + + if ( + getStaticMemberPro(path.node.left.object) === + 'exports' + ) { + let name = getStaticMemberPro(path.node.left); + + // 动态导出 + if (name === false) { + return; + } + + if (this.ingorekeys.includes(name)) { + return; + } + + generateExportNode(path, name); + } + } else if (path.node.left.object.name === 'exports') { + // 节点 exports.x = 1; + // 不包含访问子属性 exports.x.y = 1; + let name = getStaticMemberPro(path.node.left); + if ( + exportsBinding || + // 动态导出 + name === false + ) { + return; + } + + if (this.ingorekeys.includes(name)) { + return; + } + + generateExportNode(path, name); + } + } + }, + }, + + MemberExpression: { + enter: (path) => { + if ( + this.state.isDynamicUsedExportsProperty || + this.state.isUsedExportsObject + ) { + return; + } + + const moduleBinding = path.scope.getBinding('module'); + const exportsBinding = path.scope.getBinding('exports'); + + let addUsedExports = () => { + let exportsProVal = getStaticMemberPro(path.node); + + // 动态访问了 exports 上的属性 + if (exportsProVal === false) { + this.state.isDynamicUsedExportsProperty = true; + return; + } + + this.state.usedExports.add(exportsProVal); + }; + + // 连等情况 + // let a = exports.x = 1,返回true,不算对x的引用,可直接删除exports.x + // let a = exports.x.y = 1, 返回false,算对x的引用,不会删除exports.x + let checkIsAssignmentExpressionLeft = () => { + let parentPath = path.parentPath; + + if (!t.isAssignmentExpression(parentPath)) { + return false; + } + + let leftPath = parentPath.get('left'); + return leftPath === path; + }; + + // 节点 module.exports.x + if ( + t.isMemberExpression(path.node.object) && + path.node.object.object.name === 'module' + ) { + if (moduleBinding) { + return; + } + + this.state.isCJS = true; + + if (checkIsAssignmentExpressionLeft()) { + return; + } + + let staticModuleProVal = getStaticMemberPro( + path.node.object + ); + + // 动态访问了 module 上的属性 + // 无法确切的知道是否访问了 exports 属性 + // 进而无法知道访问了exports 的哪些属性 + if (staticModuleProVal === false) { + this.state.isDynamicUsedExportsProperty = true; + return; + } + + if (staticModuleProVal !== 'exports') { + return; + } + + addUsedExports(); + } else if (path.node.object.name === 'exports') { + // 节点 exports.x + if (exportsBinding) { + return; + } + + this.state.isCJS = true; + + if (checkIsAssignmentExpressionLeft()) { + return; + } + + addUsedExports(); + } else if (path.node.object.name === 'module') { + // 直接使用 module.exports 对象整体 + if (moduleBinding) { + return; + } + + this.state.isCJS = true; + + let staticModuleProVal = getStaticMemberPro(path.node); + + if (staticModuleProVal !== 'exports') { + return; + } + + // module.exports.x 情况 + // 不算对 module.exports 整体的使用 + if (t.isMemberExpression(path.parentPath)) { + return; + } + + // 到这里该语句一定严格是 module.exports + // 判断是否使用 + if (!that.checkUsed(path)) { + return; + } + + this.state.isUsedExportsObject = true; + } + }, + }, + + Identifier: { + enter: (path) => { + if ( + this.state.isDynamicUsedExportsProperty || + this.state.isUsedExportsObject + ) { + return; + } + + // 直接使用 exports 对象整体 + if ( + // 是exports + path.node.name === 'exports' && + // 父节点不是对象属性访问,例如:module.exports 或者 exports.a + !t.isMemberExpression(path.parentPath) && + // 作用域无 binding exports + !path.scope.getBinding('exports') + ) { + this.state.isCJS = true; + + if (that.checkUsed(path)) { + this.state.isUsedExportsObject = true; + } + } + }, + }, + }); + } + + // 检查是否赋值给其他变量 + checkUsed(path) { + let parentPath = path.parentPath; + + // 处于等式左边,且右边不为 Identifier + // 即语句类似于:exports = {} + + // 且该语句没有赋值给其他语句,例如: + // let a = exports = {} + // a = exports = {} + // {a: exports = {}} + // [exports = {}] 等等 + + // 这里没有去一一判断,而是作了简单处理 + // 判断 exports = {} 的父节点为作用域语句,例如: + // function(){exports = {}} + // if(a){exports = {}} + // 但这会造成漏判,例如: + // if(exports = {}){} + // 并没有将 exports 赋值给其他变量,但这里拦截不了 + if ( + t.isAssignmentExpression(parentPath) && + parentPath.get('left') === path && + !t.isIdentifier(parentPath.get('right')) && + // 父节点是赋值语句,且父节点直接在作用域语句中 + t.isScopable(parentPath.parentPath.parentPath) + ) { + return false; + } + + // Object.defineProperty(exports, "__esModule", { + // value: true + // }); + // 特殊处理,不算对 exports 整体的使用 + if (t.isCallExpression(parentPath)) { + let nextParam = path.getAllNextSiblings()[0]; + + let defineEsModule = + t.isStringLiteral(nextParam) && + nextParam.node.value === '__esModule'; + + if (defineEsModule) { + return false; + } + } + + return true; + } +} + +module.exports = { + CommonJS, +}; diff --git a/packages/wxa-cli/src/tree-shake/tree-shake.js b/packages/wxa-cli/src/tree-shake/tree-shake.js new file mode 100644 index 00000000..89d454a1 --- /dev/null +++ b/packages/wxa-cli/src/tree-shake/tree-shake.js @@ -0,0 +1,441 @@ +const {dceDeclaration, log} = require('./util'); +const generate = require('@babel/generator').default; + +class TreeShake { + // 根文件集 + roots = []; + // 由 impoet->export 组成的链 + chains = {}; + // 没有被 import 的 export 节点集合 + noReferencedExports = []; + constructor(roots) { + this.roots = roots; + } + + run() { + this.roots.forEach((root) => { + this.shake(root); + }); + + log('shook'); + + this.roots.forEach((root) => { + this.collectNotReferencedExports(root); + }); + + log('collected'); + + this.remove(); + + log('removed'); + + let contents = {}; + this.roots.forEach((root) => { + contents = {...contents, ...this.output(root)}; + }); + + log('output'); + return contents; + } + + shake(dep) { + if (dep._shook) { + return; + } + + let {imports, exports, isRoot, src: depSrc} = dep; + + log('shake src', depSrc); + + let fileExportChain = []; + this.chains[depSrc] = fileExportChain; + + // 从 import 语句开始,构建一条 import->export->export 链式关系 + let collectExportChain = (dep, childSrc, currentChain) => { + if (currentChain.length) { + let child = dep.children.find( + (child) => child.src === childSrc + ); + let exportsArray = Object.entries(child.exports); + let localIndex = exportsArray.findIndex( + ([src]) => src == child.src + ); + let localExports = null; + let externalExports = [...exportsArray]; + + if (localIndex !== -1) { + localExports = externalExports.splice(localIndex, 1)[0]; + } + + let usedExports = {}; + let nextChain = []; + + let setCurrentChain = (chainNode, childName, path) => { + let childChainNode = { + name: childName, + path, + children: [], + parent: chainNode, + dep: child, + }; + chainNode.children.push(childChainNode); + nextChain.push(childChainNode); + path.$chain = { + [childName]: childChainNode, + }; + }; + + let getExportLocalName = (path, defaultName) => { + // 兼容 exports.x = 1 节点 + if (path.$isCjsExport) { + return defaultName; + } + + let local = path.node.local; + if (local) { + return local.name; + } + + return '*'; + }; + + let addUsedExport = (src, name, path) => { + usedExports[src] = usedExports[src] || {}; + + if (name) { + usedExports[src][name] = path; + } + }; + + let collect = (chainNode, path, src, defaultLocalName) => { + let localName = ''; + + if (defaultLocalName) { + localName = defaultLocalName; + } else { + localName = getExportLocalName(path); + } + + // 对于导出其他文件模块的语句:export {a as aa} from '' + // 可以理解为通过这条语句到对应文件去寻找 a 模块 + // 如果多次解析到这条语句,并且寻找的模块名一样,那每次后面的分析都是一样的 + // 一般来说 export {a as aa} from '' 语句寻找的就是语句中定义的模块 a + // 但 export * from '',寻找的模块名可能由父节点决定 + if (path.$chain && path.$chain[localName]) { + chainNode.children.push(path.$chain[localName]); + return; + } + + setCurrentChain(chainNode, localName, path); + addUsedExport(src, localName, path); + }; + + let importAllChainNode = currentChain.find( + (node) => node.name === '*' + ); + + if (importAllChainNode) { + let importDefaultChainNode = currentChain.find( + (node) => node.name === 'default' + ); + + let markedDefalut = false; + if (localExports) { + Object.entries(localExports[1]).forEach( + ([name, path]) => { + if (name === 'default') { + if (importDefaultChainNode) { + markedDefalut = true; + setCurrentChain( + importDefaultChainNode, + 'dafault', + path + ); + } + } else { + let localName = getExportLocalName( + path, + name + ); + setCurrentChain( + importAllChainNode, + localName, + path + ); + } + } + ); + } + + externalExports.forEach(([src, exportInfo]) => { + Object.entries(exportInfo).forEach(([name, path]) => { + if ( + name === 'default' && + importDefaultChainNode && + !markedDefalut + ) { + collect(importDefaultChainNode, path, src); + } else if (name !== 'default') { + collect(importAllChainNode, path, src); + } + }); + }); + } else { + currentChain.forEach((chainNode) => { + let name = chainNode.name; + if (localExports) { + let path = localExports[1][name]; + + if (path) { + if (name === 'default') { + setCurrentChain(chainNode, 'dafault', path); + } else { + let localName = getExportLocalName( + path, + name + ); + setCurrentChain(chainNode, localName, path); + } + return; + } + } + + externalExports.forEach(([src, exportInfo]) => { + let path = exportInfo[name]; + + if (path) { + collect(chainNode, path, src); + } + + path = exportInfo['*']; + + if (path) { + collect(chainNode, path, src, name); + } + }); + }); + } + + Object.entries(usedExports).forEach((src, value) => { + let childUsedNames = Object.keys(value); + let childChain = childUsedNames.map((n) => { + return nextChain.find( + (chainNode) => chainNode.name === n + ); + }); + + collectExportChain(child, src, childChain); + }); + } + }; + + Object.entries(imports).forEach(([src, value]) => { + let currentChain = []; + + Object.entries(value).forEach(([name, path]) => { + let chainNode = { + parent: null, + name, + path, + children: [], + dep, + }; + fileExportChain.push(chainNode); + currentChain.push(chainNode); + }); + + collectExportChain(dep, src, currentChain); + }); + + // 根节点的 export 语句默认全部保留 + // 所以还需要处理根节点的 export 语句(export {} from '') + if (isRoot) { + Object.entries(exports).forEach(([src, exportInfo]) => { + if (src !== dep.src) { + let currentChain = []; + + Object.entries(exportInfo).forEach(([name, path]) => { + let chainNode = { + parent: null, + name, + path, + children: [], + dep, + }; + fileExportChain.push(chainNode); + currentChain.push(chainNode); + }); + + collectExportChain(dep, src, currentChain); + } + }); + } + + let markExport = () => { + let visitedChainNodes = []; + + let findLastChainNode = (chainNode) => { + // 某个节点在链上出现过两次,这条链不会找到最终导出的模块 + if (visitedChainNodes.includes(chainNode)) { + return; + } + + visitedChainNodes.push(chainNode); + + if (!chainNode.children.length) { + let exportPath = chainNode.path; + + // 最后一个节点是文件内导出(不是 export a from '') + // 才算是找到最终导出的节点 + if (exportPath.node && !exportPath.node.source) { + visitedChainNodes.forEach((visitedNode, index) => { + // 第一个节点是 import 节点或者 root 的 export 节点 + // 跳过 + if (index === 0) { + return; + } + + let {path} = visitedNode; + + // 这条链上的所有 export 节点的 $extReferences 属性添加 import 节点 + // 表示它们被导入过 + path.$extReferences.add(visitedChainNodes[0].path); + }); + } + } else { + chainNode.children.forEach((child) => { + findLastChainNode(child); + }); + } + + visitedChainNodes.pop(); + }; + + fileExportChain.forEach((chainNode) => { + findLastChainNode(chainNode); + }); + }; + + markExport(); + + dep._shook = true; + dep.children.forEach((child) => this.shake(child)); + } + + // 收集没有被导入过的 export 节点 + collectNotReferencedExports(dep) { + if (dep._colletced) { + return; + } + + let {exports, src} = dep; + + log('collect src', src); + + Object.entries(exports).forEach(([src, value]) => { + Object.entries(value).forEach(([name, path]) => { + if (!path.$extReferences.size) { + this.noReferencedExports.push({path, dep, name}); + } + }); + }); + + dep._colletced = true; + + dep.children.forEach((child) => + this.collectNotReferencedExports(child) + ); + } + + remove() { + let visitedChainNodes = []; + let removeExtReference = (chainNode, importPath) => { + // 循环依赖 + if (visitedChainNodes.includes(chainNode)) { + return; + } + + visitedChainNodes.push(chainNode); + chainNode.children.forEach((childNode) => { + childNode.path.$extReferences.delete(importPath); + + if (childNode.path.$extReferences.size === 0) { + this.noReferencedExports.push({ + path: childNode.path, + dep: childNode.dep, + name: childNode.name, + }); + } + + removeExtReference(childNode, importPath); + }); + visitedChainNodes.pop(); + }; + + for (let i = 0; i < this.noReferencedExports.length; i++) { + const {path: exportPath, dep, name} = this.noReferencedExports[i]; + let {commonjs, topScope, src} = dep; + + if (exportPath.$isCjsExport) { + log(src, name); + commonjs.deleteCJSExport(name); + } else { + log(src, name); + exportPath.remove(); + } + + let importPaths = dceDeclaration(topScope); + // 如果删除了 import 节点 + // 对应链上的所有 export 节点都删除这个 import 节点 + if (importPaths.length) { + let chain = this.chains[src]; + + importPaths.forEach((importPath) => { + let chainNode = chain.find( + (chainNode) => chainNode.path === importPath + ); + + removeExtReference(chainNode, importPath); + }); + } + } + } + + output(dep) { + let contents = {}; + + let run = (dep) => { + let {ast, code, src} = dep; + + if (dep._output) { + return; + } + + const {code: outputCode} = generate( + ast, + { + decoratorsBeforeExport: true, + }, + code + ); + + contents[src] = { + code, + formattedCode: outputCode, + src, + }; + + dep._output = true; + + dep.children.forEach((child) => { + run(child); + }); + }; + + run(dep); + + return contents; + } +} + +module.exports = {TreeShake}; diff --git a/packages/wxa-cli/src/tree-shake/util.js b/packages/wxa-cli/src/tree-shake/util.js new file mode 100644 index 00000000..9711ea67 --- /dev/null +++ b/packages/wxa-cli/src/tree-shake/util.js @@ -0,0 +1,321 @@ +let fs = require('fs'); +let path = require('path'); +let mkdirp = require('mkdirp'); +const {parse} = require('@babel/parser'); +const t = require('@babel/types'); +const traverse = require('@babel/traverse').default; +let findRoot = require('find-root'); +let config = require('./config'); + +function readFile(p) { + let rst = ''; + p = typeof p === 'object' ? path.join(p.dir, p.base) : p; + try { + rst = fs.readFileSync(p, 'utf-8'); + } catch (e) { + rst = null; + } + + return rst; +} + +function writeFile(p, data) { + let opath = typeof p === 'string' ? path.parse(p) : p; + mkdirp.sync(opath.dir); + fs.writeFileSync(p, data); +} + +function isFile(p) { + p = typeof p === 'object' ? path.join(p.dir, p.base) : p; + if (!fs.existsSync(p)) return false; + return fs.statSync(p).isFile(); +} + +function isDir(p) { + if (!fs.existsSync(p)) { + return false; + } + + return fs.statSync(p).isDirectory(); +} + +function getPkgConfig(npmSrc, lib) { + let uri = path.join(npmSrc, lib); + let location = findRoot(uri); + let content = readFile(path.join(location, 'package.json')); + try { + content = JSON.parse(content); + } catch (e) { + content = null; + } + + return content; +} + +/** + * + * @description 将import路径解析为绝对路径 + * @example + * resolveDepSrc({ + fileSrc: path.join(process.cwd(), './src/a.js'), + depSrc: '@/b', + root: 'src', + alias: { + '@': path.join(process.cwd(), '/src/xxx'), + }, + }) + * @param {string} fileSrc 文件路径 + * @param {string} depSrc impot 引入的路径("./a", "a", "@/a", "/a") + * @param {string} root depsrc 为绝对路径(例如:/a)时相对的目录 + * @param {string} alias 路径别名,例如:{'@': 'src'} + * @param {string} npm npm 目录,例如:node_modules + * @return {string} 依赖的绝对路径 + */ +function resolveDepSrc({fileSrc, depSrc, root, alias, npm}) { + let cwd = process.cwd(); + + let getFileSrc = (src) => { + if (isDir(src)) { + return path.join(src, 'index.js'); + } + + if (!src.endsWith('.js')) { + return src + '.js'; + } + + return src; + }; + + if (alias) { + let aliasNames = Object.keys(alias); + let absoluteSrc = ''; + let matched = aliasNames.some((aliasName) => { + if (depSrc.startsWith(aliasName + '/')) { + let aliasSrc = alias[aliasName]; + + absoluteSrc = path.resolve( + cwd, + aliasSrc, + depSrc.replace(aliasName, '.') + ); + return true; + } + }); + + if (matched) { + return getFileSrc(absoluteSrc); + } + } + + if (depSrc.startsWith('/')) { + return getFileSrc(path.resolve(cwd, root, depSrc.replace('/', ''))); + } + + if (depSrc.startsWith('./') || depSrc.startsWith('../')) { + let fileDir = path.dirname(fileSrc); + return getFileSrc(path.resolve(fileDir, depSrc)); + } + + let npmSrc = path.join(cwd, npm); + let absoluteSrc = path.join(npmSrc, depSrc); + + if (!absoluteSrc.endsWith('.js')) { + absoluteSrc += '.js'; + } + + if (isFile(absoluteSrc)) { + return absoluteSrc; + } + + let pkg = getPkgConfig(npmSrc, depSrc); + + if (!pkg) { + throw new Error('找不到模块' + depSrc); + } + + let main = pkg.main || 'index.js'; + // 优先使用依赖的 browser 版本 + if (pkg.browser && typeof pkg.browser === 'string') { + main = pkg.browser; + } + + return getFileSrc(path.join(npmSrc, depSrc, main)); +} + +function parseESCode(code) { + return parse(code, config.parseOptions); +} + +function isChildNode(parent, child) { + if (parent === child) { + return true; + } + + let is = false; + + traverse(parent, { + noScope: true, + enter(path) { + let {node} = path; + if (node === child) { + is = true; + path.stop(); + } + }, + }); + + return is; +} + +// 删除 scope 下未引用的 binding +function dceDeclaration(scope) { + let importPaths = []; + + let doDce = (scope) => { + let hasRemoved = false; + + // 删除节点并不会自动更新相关binding的referenced等信息 + // 这里是重新收集bindings信息 + scope.crawl(); + + let removeObjectPattern = (binding) => { + let proPath = null; + + traverse(binding.path.node, { + noScope: true, + ObjectProperty: { + enter: (path) => { + if (path.node.value === binding.identifier) { + proPath = path; + path.stop(); + } + }, + }, + }); + + return proPath; + }; + + let remove = (name, scope, binding) => { + let removedPath = binding.path; + let bindingPath = binding.path; + + if ( + t.isImportDefaultSpecifier(bindingPath) || + t.isImportNamespaceSpecifier(bindingPath) || + t.isImportSpecifier(bindingPath) + ) { + importPaths.push(bindingPath); + } + + // let {x: x1} = {x: 1, y2: 2}; + if (t.isVariableDeclarator(bindingPath)) { + let id = bindingPath.node.id; + + if (t.isObjectPattern(id)) { + removedPath = removeObjectPattern(binding) || removedPath; + } + } + + removedPath.remove(); + scope.removeOwnBinding(name); + hasRemoved = true; + }; + + Object.entries(scope.bindings).forEach(([name, binding]) => { + if (binding.referenced) { + return; + } + + let bindingPath = binding.path; + + // 类似于let a = function ff(){} + // ff 是函数内部作用域的binding,ff不应该被删除 + // if (t.isFunctionExpression(bindingPath)) { + // return; + // } + + // // try...catch(e),e没访问到时不该删除整个catch语句 + // if (t.isCatchClause(bindingPath)) { + // return; + // } + + // // function(a, b) {}; + // // let t = function({dataset: {name, opts={}}}, [asq, ttqw]) {}; + // // 函数的参数 + // if ( + // t.isFunctionExpression(bindingPath.parentPath) || + // t.isFunctionDeclaration(bindingPath.parentPath) + // ) { + // return; + // } + + // // let [zz, xx, cc] = [1, 2, 3]; + // // let {x: x1} = {x: 1, y2: 2}; + // if ( + // t.isVariableDeclarator(bindingPath) && + // t.isArrayPattern(bindingPath.node.id) + // ) { + // return; + // } + + // // 未知 + // if (t.isArrayPattern(bindingPath)) { + // return; + // } + + if ( + // 过滤 let [zz, xx, cc] = [1, 2, 3]; + // 变量声明语句可能有副作用,不能简单去除 + // 例如: + // let a = obj.x,obj.x 可能有访问器属性。 + // let a = b = 1 连等 + // let a = fn() 非纯函数运行 + // 先处理为 init 为 identifier、函数、class、Literals 时,可以删除 + (t.isVariableDeclarator(bindingPath) && + !t.isArrayPattern(bindingPath.node.id) && + (t.isIdentifier(bindingPath.node.init) || + t.isLiteral(bindingPath.node.init) || + t.isArrowFunctionExpression(bindingPath.node.init) || + t.isFunctionExpression(bindingPath.node.init) || + t.isClassExpression(bindingPath.node.init))) || + t.isClassDeclaration(bindingPath) || + t.isFunctionDeclaration(bindingPath) || + t.isImportDefaultSpecifier(bindingPath) || + t.isImportNamespaceSpecifier(bindingPath) || + t.isImportSpecifier(bindingPath) + ) { + remove(name, scope, binding); + } + }); + + // 处理声明之间循环引用 + // 当一个声明未被使用时,那该声明所引用的其他声明不算真正使用 + if (hasRemoved) { + doDce(scope); + } + }; + + doDce(scope); + + return importPaths; +} + +function unique(ary) { + return [...new Set(ary)]; +} + +function log(...params) { + config.debug && log(...params); +} + +module.exports = { + readFile, + writeFile, + resolveDepSrc, + parseESCode, + dceDeclaration, + isChildNode, + unique, + log, +};