UNPKG

5.13 kBJavaScriptView Raw
1'use strict'
2
3const fs = require('fs')
4const path = require('path')
5const babylon = require('babylon')
6const traverse = require('babel-traverse')['default']
7const t = require('babel-types')
8const chalk = require('chalk')
9
10class VendorExtracter {
11 constructor (opts) {
12 this.excludes = [] // 一些因为 alias 而可能会被误判成 vendor 的模块
13 this.defaultExt = '.js'
14 this.fileModuleFomatter = opts.fileModuleFomatter || (i => i)
15 this.moduleFilter = opts.moduleFilter || (_ => true)
16 }
17
18 parse (entryFiles) {
19 if (!Array.isArray(entryFiles)) entryFiles = [ entryFiles ]
20
21 let vendorList = this._parseModules(entryFiles, [], [])
22 return vendorList
23 }
24
25 _parseModules (files, parsed, result) {
26 if (!Array.isArray(files) || files.length <= 0) return result
27
28 const fileModules = [], parsedFileModules = []
29 files.forEach(file => {
30 // 保证其为绝对路径
31 file = path.resolve(file)
32 parsedFileModules.push(file)
33
34 // 1. 读取入口点文件,解析其代码
35 let codeContent
36 try {
37 codeContent = fs.readFileSync(file, { encoding: 'utf-8' })
38 }
39 catch (e) {
40 console.log(chalk.red(`[Extracter] 读取入口 ${file} 失败,${e.message}`))
41 return
42 }
43
44 let modules = this._parseModulesFromCode(codeContent, file)
45 modules = modules.map(this.fileModuleFomatter)
46 modules = modules.filter(this.moduleFilter)
47
48 // 2. 根据 node.js 模块加载机制
49 // - 获取所有依赖模块
50 // - 过滤出来自 npm/tnpm 的模块
51 modules.forEach(moduleName => {
52 moduleName = this.fileModuleFomatter(moduleName)
53
54 if (result.indexOf(moduleName) < 0 && this._isFromNodeModules(moduleName)) {
55 result.push(moduleName)
56 }
57
58 if (!this._isFromNodeModules(moduleName)) {
59 let base = path.dirname(file)
60 let location = path.resolve(base, moduleName)
61
62 if(!path.extname(location)) {
63 location += this.defaultExt
64 }
65 fileModules.push(location)
66 }
67 })
68 })
69
70 parsed = [].concat(parsed, parsedFileModules)
71 files = fileModules.filter(f => parsed.indexOf(f) < 0)
72
73 return this._parseModules(files, parsed, result)
74 }
75
76 _isFromNodeModules (moduleName) {
77 // 非 nodemodules 模块的例子:
78 // - './xxx'
79 // - '/xxx/xxx'
80 // - '../xxx/xxx'
81 return !/^[\./]/.test(moduleName)
82 }
83
84 _parseModulesFromCode (codeContent, fileName) {
85 const modules = []
86 const _self = this
87 try {
88 const ast = babylon.parse(codeContent, {
89 sourceType: 'module', // or script
90 plugins: [
91 "jsx",
92 "flow",
93 "asyncFunctions",
94 "classConstructorCall",
95 "doExpressions",
96 "trailingFunctionCommas",
97 "objectRestSpread",
98 "decorators",
99 "classProperties",
100 "exportExtensions",
101 "exponentiationOperator",
102 "asyncGenerators",
103 "functionBind",
104 "functionSent"
105 ]
106 })
107
108 console.log(`[Extracter] 开始解析 ${fileName}`);
109
110 traverse(ast, {
111 enter (path) {
112 // 遍历出
113 // - ImportDeclaration
114 // - CallExpression 中 callee 为 Identifier 且 name 为 require
115 // - 所有 FunctionExpression 中的 require 表达式
116
117 const node = path.node
118 if (t.isImportDeclaration(node)) {
119 modules.push(node.source.value)
120 }
121 if (t.isCallExpression(node) && node.callee.name == 'require') {
122 const arg = node.arguments[0]
123 if (!t.isStringLiteral(arg)) {
124 console.log(`[Extracter] 解析 ${fileName} 时发现有模块不是以字符串字面量的形式引入的,当前版本不支持该形式`)
125 return []
126 }
127
128 // 可能会有 require('lodash/func') 的写法
129 // - 它的依赖应该被判断成 lodash
130 // - 排除 @ali/dpl 这样的 scoped module
131 // - 目前不支持抽取模块中部分文件的打包方式
132 if (_self._isFromNodeModules(arg.value)) {
133 const sub = arg.value.split('/')
134 if (arg.value[0] == '@') { // scoped module
135 modules.push(sub.slice(0, 2).join('/'))
136 }
137 else modules.push(sub[0])
138 }
139 else modules.push(arg.value)
140 }
141 }
142 })
143 }
144 catch (e) {
145 console.log(chalk.red(`[Extracter] 解析文件 ${fileName} 代码失败,${e.message}`))
146 return []
147 }
148
149 return modules
150 }
151
152 output (vendorList) {
153 const source =
154 'module.exports = {' + '\n' +
155 vendorList.map((name, i) => {
156 if (i == vendorList.length - 1) {
157 return ` "${name}": require('${name}')`
158 }
159 else return ` "${name}": require('${name}'),`
160 }).join('\n') + '\n' +
161 '};'
162
163 return source
164 }
165
166 parseAndOutput (entryFiles) {
167 const vendorList = this.parse(entryFiles)
168 return this.output(vendorList)
169 }
170}
171
172module.exports = VendorExtracter