UNPKG

9.72 kBJavaScriptView Raw
1"use strict";
2
3const fs = require('fs');
4
5const _ = require('lodash');
6
7const acorn = require('acorn');
8
9const walk = require('acorn-walk');
10
11module.exports = {
12 parseBundle
13};
14
15function parseBundle(bundlePath) {
16 const content = fs.readFileSync(bundlePath, 'utf8');
17 const ast = acorn.parse(content, {
18 sourceType: 'script',
19 // I believe in a bright future of ECMAScript!
20 // Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
21 // Seems like `acorn` supports such weird option value.
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 ( // Webpack 5 stores modules in the the top-level IIFE
34 state.expressionStatementDepth === 1 && ast.body.includes(node) && isIIFE(node)) {
35 const fn = getIIFECallExpression(node);
36
37 if ( // It should not contain neither arguments
38 fn.arguments.length === 0 && // ...nor parameters
39 fn.callee.params.length === 0) {
40 // Modules are stored in the very first variable declaration as hash
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; // Modules are stored in exports.modules:
66 // exports.modules = {};
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; // Main chunk with webpack loader.
81 // Modules are stored in first argument:
82 // (function (...) {...})(<modules>)
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 } // Async Webpack < v4 chunk without webpack loader.
88 // webpackJsonp([<chunks>], <modules>, ...)
89 // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
90
91
92 if (node.callee.type === 'Identifier' && mayBeAsyncChunkArguments(args) && isModulesList(args[1])) {
93 state.locations = getModulesLocations(args[1]);
94 return;
95 } // Async Webpack v4 chunk without webpack loader.
96 // (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
97 // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
98
99
100 if (isAsyncChunkPushExpression(node)) {
101 state.locations = getModulesLocations(args[0].elements[1]);
102 return;
103 } // Webpack v4 WebWorkerChunkTemplatePlugin
104 // globalObject.chunkCallbackName([<chunks>],<modules>, ...);
105 // Both globalObject and chunkCallbackName can be changed through the config, so we can't check them.
106
107
108 if (isAsyncWebWorkerChunkExpression(node)) {
109 state.locations = getModulesLocations(args[1]);
110 return;
111 } // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
112 // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
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 * Returns bundle source except modules
135 */
136
137
138function 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
154function isIIFE(node) {
155 return node.type === 'ExpressionStatement' && (node.expression.type === 'CallExpression' || node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression');
156}
157
158function getIIFECallExpression(node) {
159 if (node.expression.type === 'UnaryExpression') {
160 return node.expression.argument;
161 } else {
162 return node.expression;
163 }
164}
165
166function isModulesList(node) {
167 return isSimpleModulesList(node) || // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
168 isOptimizedModulesArray(node);
169}
170
171function isSimpleModulesList(node) {
172 return (// Modules are contained in hash. Keys are module ids.
173 isModulesHash(node) || // Modules are contained in array. Indexes are module ids.
174 isModulesArray(node)
175 );
176}
177
178function isModulesHash(node) {
179 return node.type === 'ObjectExpression' && node.properties.map(node => node.value).every(isModuleWrapper);
180}
181
182function isModulesArray(node) {
183 return node.type === 'ArrayExpression' && node.elements.every(elem => // Some of array items may be skipped because there is no module with such id
184 !elem || isModuleWrapper(elem));
185}
186
187function isOptimizedModulesArray(node) {
188 // Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
189 // https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
190 // The `<minimum ID>` + array indexes are module ids
191 return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && // Make sure the object called is `Array(<some number>)`
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]) && // Make sure the property X called for `Array(<some number>).X` is `concat`
193 node.callee.property.type === 'Identifier' && node.callee.property.name === 'concat' && // Make sure exactly one array is passed in to `concat`
194 node.arguments.length === 1 && isModulesArray(node.arguments[0]);
195}
196
197function isModuleWrapper(node) {
198 return (// It's an anonymous function expression that wraps module
199 (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id || // If `DedupePlugin` is used it can be an ID of duplicated module...
200 isModuleId(node) || // or an array of shape [<module_id>, ...args]
201 node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0])
202 );
203}
204
205function isModuleId(node) {
206 return node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string');
207}
208
209function isNumericId(node) {
210 return node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0;
211}
212
213function isChunkIds(node) {
214 // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
215 return node.type === 'ArrayExpression' && node.elements.every(isModuleId);
216}
217
218function 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
226function mayBeAsyncChunkArguments(args) {
227 return args.length >= 2 && isChunkIds(args[0]);
228}
229
230function 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
239function getModulesLocations(node) {
240 if (node.type === 'ObjectExpression') {
241 // Modules hash
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 // Modules array or optimized array
254 const minId = isOptimizedArray ? // Get the [minId] value from the Array() call first argument literal value
255 node.callee.object.arguments[0].value : // `0` for simple array
256 0;
257 const modulesNodes = isOptimizedArray ? // The modules reside in the `concat()` function call arguments
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
271function getModuleLocation(node) {
272 return {
273 start: node.start,
274 end: node.end
275 };
276}
\No newline at end of file