1 | "use strict";
|
2 |
|
3 | const fs = require('fs');
|
4 |
|
5 | const _ = require('lodash');
|
6 |
|
7 | const acorn = require('acorn');
|
8 |
|
9 | const walk = require('acorn-walk');
|
10 |
|
11 | module.exports = {
|
12 | parseBundle
|
13 | };
|
14 |
|
15 | function parseBundle(bundlePath) {
|
16 | const content = fs.readFileSync(bundlePath, 'utf8');
|
17 | const ast = acorn.parse(content, {
|
18 | sourceType: 'script',
|
19 |
|
20 |
|
21 |
|
22 | ecmaVersion: 2050
|
23 | });
|
24 | const walkState = {
|
25 | locations: null,
|
26 | expressionStatementDepth: 0
|
27 | };
|
28 | walk.recursive(ast, walkState, {
|
29 | ExpressionStatement(node, state, c) {
|
30 | if (state.locations) return;
|
31 | state.expressionStatementDepth++;
|
32 |
|
33 | if (
|
34 | state.expressionStatementDepth === 1 && ast.body.includes(node) && isIIFE(node)) {
|
35 | const fn = getIIFECallExpression(node);
|
36 |
|
37 | if (
|
38 | fn.arguments.length === 0 &&
|
39 | fn.callee.params.length === 0) {
|
40 |
|
41 | const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === 'VariableDeclaration');
|
42 |
|
43 | if (firstVariableDeclaration) {
|
44 | for (const declaration of firstVariableDeclaration.declarations) {
|
45 | if (declaration.init) {
|
46 | state.locations = getModulesLocations(declaration.init);
|
47 |
|
48 | if (state.locations) {
|
49 | break;
|
50 | }
|
51 | }
|
52 | }
|
53 | }
|
54 | }
|
55 | }
|
56 |
|
57 | if (!state.locations) {
|
58 | c(node.expression, state);
|
59 | }
|
60 |
|
61 | state.expressionStatementDepth--;
|
62 | },
|
63 |
|
64 | AssignmentExpression(node, state) {
|
65 | if (state.locations) return;
|
66 |
|
67 |
|
68 | const {
|
69 | left,
|
70 | right
|
71 | } = node;
|
72 |
|
73 | if (left && left.object && left.object.name === 'exports' && left.property && left.property.name === 'modules' && isModulesHash(right)) {
|
74 | state.locations = getModulesLocations(right);
|
75 | }
|
76 | },
|
77 |
|
78 | CallExpression(node, state, c) {
|
79 | if (state.locations) return;
|
80 | const args = node.arguments;
|
81 |
|
82 |
|
83 |
|
84 | if (node.callee.type === 'FunctionExpression' && !node.callee.id && args.length === 1 && isSimpleModulesList(args[0])) {
|
85 | state.locations = getModulesLocations(args[0]);
|
86 | return;
|
87 | }
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | if (node.callee.type === 'Identifier' && mayBeAsyncChunkArguments(args) && isModulesList(args[1])) {
|
93 | state.locations = getModulesLocations(args[1]);
|
94 | return;
|
95 | }
|
96 |
|
97 |
|
98 |
|
99 |
|
100 | if (isAsyncChunkPushExpression(node)) {
|
101 | state.locations = getModulesLocations(args[0].elements[1]);
|
102 | return;
|
103 | }
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | if (isAsyncWebWorkerChunkExpression(node)) {
|
109 | state.locations = getModulesLocations(args[1]);
|
110 | return;
|
111 | }
|
112 |
|
113 |
|
114 |
|
115 | args.forEach(arg => c(arg, state));
|
116 | }
|
117 |
|
118 | });
|
119 | let modules;
|
120 |
|
121 | if (walkState.locations) {
|
122 | modules = _.mapValues(walkState.locations, loc => content.slice(loc.start, loc.end));
|
123 | } else {
|
124 | modules = {};
|
125 | }
|
126 |
|
127 | return {
|
128 | modules,
|
129 | src: content,
|
130 | runtimeSrc: getBundleRuntime(content, walkState.locations)
|
131 | };
|
132 | }
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | function getBundleRuntime(content, modulesLocations) {
|
139 | const sortedLocations = Object.values(modulesLocations || {}).sort((a, b) => a.start - b.start);
|
140 | let result = '';
|
141 | let lastIndex = 0;
|
142 |
|
143 | for (const {
|
144 | start,
|
145 | end
|
146 | } of sortedLocations) {
|
147 | result += content.slice(lastIndex, start);
|
148 | lastIndex = end;
|
149 | }
|
150 |
|
151 | return result + content.slice(lastIndex, content.length);
|
152 | }
|
153 |
|
154 | function isIIFE(node) {
|
155 | return node.type === 'ExpressionStatement' && (node.expression.type === 'CallExpression' || node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression');
|
156 | }
|
157 |
|
158 | function getIIFECallExpression(node) {
|
159 | if (node.expression.type === 'UnaryExpression') {
|
160 | return node.expression.argument;
|
161 | } else {
|
162 | return node.expression;
|
163 | }
|
164 | }
|
165 |
|
166 | function isModulesList(node) {
|
167 | return isSimpleModulesList(node) ||
|
168 | isOptimizedModulesArray(node);
|
169 | }
|
170 |
|
171 | function isSimpleModulesList(node) {
|
172 | return (
|
173 | isModulesHash(node) ||
|
174 | isModulesArray(node)
|
175 | );
|
176 | }
|
177 |
|
178 | function isModulesHash(node) {
|
179 | return node.type === 'ObjectExpression' && node.properties.map(node => node.value).every(isModuleWrapper);
|
180 | }
|
181 |
|
182 | function isModulesArray(node) {
|
183 | return node.type === 'ArrayExpression' && node.elements.every(elem =>
|
184 | !elem || isModuleWrapper(elem));
|
185 | }
|
186 |
|
187 | function isOptimizedModulesArray(node) {
|
188 |
|
189 |
|
190 |
|
191 | return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' &&
|
192 | node.callee.object.type === 'CallExpression' && node.callee.object.callee.type === 'Identifier' && node.callee.object.callee.name === 'Array' && node.callee.object.arguments.length === 1 && isNumericId(node.callee.object.arguments[0]) &&
|
193 | node.callee.property.type === 'Identifier' && node.callee.property.name === 'concat' &&
|
194 | node.arguments.length === 1 && isModulesArray(node.arguments[0]);
|
195 | }
|
196 |
|
197 | function isModuleWrapper(node) {
|
198 | return (
|
199 | (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id ||
|
200 | isModuleId(node) ||
|
201 | node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0])
|
202 | );
|
203 | }
|
204 |
|
205 | function isModuleId(node) {
|
206 | return node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string');
|
207 | }
|
208 |
|
209 | function isNumericId(node) {
|
210 | return node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0;
|
211 | }
|
212 |
|
213 | function isChunkIds(node) {
|
214 |
|
215 | return node.type === 'ArrayExpression' && node.elements.every(isModuleId);
|
216 | }
|
217 |
|
218 | function isAsyncChunkPushExpression(node) {
|
219 | const {
|
220 | callee,
|
221 | arguments: args
|
222 | } = node;
|
223 | return callee.type === 'MemberExpression' && callee.property.name === 'push' && callee.object.type === 'AssignmentExpression' && args.length === 1 && args[0].type === 'ArrayExpression' && mayBeAsyncChunkArguments(args[0].elements) && isModulesList(args[0].elements[1]);
|
224 | }
|
225 |
|
226 | function mayBeAsyncChunkArguments(args) {
|
227 | return args.length >= 2 && isChunkIds(args[0]);
|
228 | }
|
229 |
|
230 | function isAsyncWebWorkerChunkExpression(node) {
|
231 | const {
|
232 | callee,
|
233 | type,
|
234 | arguments: args
|
235 | } = node;
|
236 | return type === 'CallExpression' && callee.type === 'MemberExpression' && args.length === 2 && isChunkIds(args[0]) && isModulesList(args[1]);
|
237 | }
|
238 |
|
239 | function getModulesLocations(node) {
|
240 | if (node.type === 'ObjectExpression') {
|
241 |
|
242 | const modulesNodes = node.properties;
|
243 | return modulesNodes.reduce((result, moduleNode) => {
|
244 | const moduleId = moduleNode.key.name || moduleNode.key.value;
|
245 | result[moduleId] = getModuleLocation(moduleNode.value);
|
246 | return result;
|
247 | }, {});
|
248 | }
|
249 |
|
250 | const isOptimizedArray = node.type === 'CallExpression';
|
251 |
|
252 | if (node.type === 'ArrayExpression' || isOptimizedArray) {
|
253 |
|
254 | const minId = isOptimizedArray ?
|
255 | node.callee.object.arguments[0].value :
|
256 | 0;
|
257 | const modulesNodes = isOptimizedArray ?
|
258 | node.arguments[0].elements : node.elements;
|
259 | return modulesNodes.reduce((result, moduleNode, i) => {
|
260 | if (moduleNode) {
|
261 | result[i + minId] = getModuleLocation(moduleNode);
|
262 | }
|
263 |
|
264 | return result;
|
265 | }, {});
|
266 | }
|
267 |
|
268 | return {};
|
269 | }
|
270 |
|
271 | function getModuleLocation(node) {
|
272 | return {
|
273 | start: node.start,
|
274 | end: node.end
|
275 | };
|
276 | } |
\ | No newline at end of file |