UNPKG

6.6 kBJavaScriptView Raw
1const fs = require('fs');
2const _ = require('lodash');
3const acorn = require('acorn');
4const walk = require('acorn/dist/walk');
5
6module.exports = {
7 parseBundle
8};
9
10function parseBundle(bundlePath) {
11 const content = fs.readFileSync(bundlePath, 'utf8');
12 const ast = acorn.parse(content, {
13 sourceType: 'script',
14 // I believe in a bright future of ECMAScript!
15 // Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
16 // Seems like `acorn` supports such weird option value.
17 ecmaVersion: 2050
18 });
19
20 const walkState = {
21 locations: null
22 };
23
24 walk.recursive(
25 ast,
26 walkState,
27 {
28 CallExpression(node, state, c) {
29 if (state.sizes) return;
30
31 const args = node.arguments;
32
33 // Additional bundle without webpack loader.
34 // Modules are stored in second argument, after chunk ids:
35 // webpackJsonp([<chunks>], <modules>, ...)
36 // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
37 if (
38 node.callee.type === 'Identifier' &&
39 args.length >= 2 &&
40 isArgumentContainsChunkIds(args[0]) &&
41 isArgumentContainsModulesList(args[1])
42 ) {
43 state.locations = getModulesLocationFromFunctionArgument(args[1]);
44 return;
45 }
46
47 // Additional bundle without webpack loader, with module IDs optimized.
48 // Modules are stored in second arguments Array(n).concat() call
49 // webpackJsonp([<chunks>], Array([minimum ID]).concat([<module>, <module>, ...]))
50 // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
51 if (
52 node.callee.type === 'Identifier' &&
53 (args.length === 2 || args.length === 3) &&
54 isArgumentContainsChunkIds(args[0]) &&
55 isArgumentArrayConcatContainingChunks(args[1])
56 ) {
57 state.locations = getModulesLocationFromArrayConcat(args[1]);
58 return;
59 }
60
61 // Main bundle with webpack loader
62 // Modules are stored in first argument:
63 // (function (...) {...})(<modules>)
64 if (
65 node.callee.type === 'FunctionExpression' &&
66 !node.callee.id &&
67 args.length === 1 &&
68 isArgumentContainsModulesList(args[0])
69 ) {
70 state.locations = getModulesLocationFromFunctionArgument(args[0]);
71 return;
72 }
73
74 // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
75 // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
76 _.each(args, arg => c(arg, state));
77 }
78 }
79 );
80
81 if (!walkState.locations) {
82 return null;
83 }
84
85 return {
86 src: content,
87 modules: _.mapValues(walkState.locations,
88 loc => content.slice(loc.start, loc.end)
89 )
90 };
91}
92
93function isArgumentContainsChunkIds(arg) {
94 // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
95 return (arg.type === 'ArrayExpression' && _.every(arg.elements, isModuleId));
96}
97
98function isArgumentContainsModulesList(arg) {
99 if (arg.type === 'ObjectExpression') {
100 return _(arg.properties)
101 .map('value')
102 .every(isModuleWrapper);
103 }
104
105 if (arg.type === 'ArrayExpression') {
106 // Modules are contained in array.
107 // Array indexes are module ids
108 return _.every(arg.elements, elem =>
109 // Some of array items may be skipped because there is no module with such id
110 !elem ||
111 isModuleWrapper(elem)
112 );
113 }
114
115 return false;
116}
117
118function isArgumentArrayConcatContainingChunks(arg) {
119 if (
120 arg.type === 'CallExpression' &&
121 arg.callee.type === 'MemberExpression' &&
122 // Make sure the object called is `Array(<some number>)`
123 arg.callee.object.type === 'CallExpression' &&
124 arg.callee.object.callee.type === 'Identifier' &&
125 arg.callee.object.callee.name === 'Array' &&
126 arg.callee.object.arguments.length === 1 &&
127 isNumericId(arg.callee.object.arguments[0]) &&
128 // Make sure the property X called for `Array(<some number>).X` is `concat`
129 arg.callee.property.type === 'Identifier' &&
130 arg.callee.property.name === 'concat' &&
131 // Make sure exactly one array is passed in to `concat`
132 arg.arguments.length === 1 &&
133 arg.arguments[0].type === 'ArrayExpression'
134 ) {
135 // Modules are contained in `Array(<minimum ID>).concat(` array:
136 // https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
137 // The `<minimum ID>` + array indexes are module ids
138 return true;
139 }
140
141 return false;
142}
143
144function isModuleWrapper(node) {
145 return (
146 // It's an anonymous function expression that wraps module
147 ((node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id) ||
148 // If `DedupePlugin` is used it can be an ID of duplicated module...
149 isModuleId(node) ||
150 // or an array of shape [<module_id>, ...args]
151 (node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0]))
152 );
153}
154
155function isModuleId(node) {
156 return (node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string'));
157}
158
159function isNumericId(node) {
160 return (node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0);
161}
162
163function getModulesLocationFromFunctionArgument(arg) {
164 if (arg.type === 'ObjectExpression') {
165 const modulesNodes = arg.properties;
166
167 return _.transform(modulesNodes, (result, moduleNode) => {
168 const moduleId = moduleNode.key.name || moduleNode.key.value;
169
170 result[moduleId] = getModuleLocation(moduleNode.value);
171 }, {});
172 }
173
174 if (arg.type === 'ArrayExpression') {
175 const modulesNodes = arg.elements;
176
177 return _.transform(modulesNodes, (result, moduleNode, i) => {
178 if (!moduleNode) return;
179
180 result[i] = getModuleLocation(moduleNode);
181 }, {});
182 }
183
184 return {};
185}
186
187function getModulesLocationFromArrayConcat(arg) {
188 // arg(CallExpression) =
189 // Array([minId]).concat([<minId module>, <minId+1 module>, ...])
190 //
191 // Get the [minId] value from the Array() call first argument literal value
192 const minId = arg.callee.object.arguments[0].value;
193 // The modules reside in the `concat()` function call arguments
194 const modulesNodes = arg.arguments[0].elements;
195
196 return _.transform(modulesNodes, (result, moduleNode, i) => {
197 if (!moduleNode) return;
198
199 result[i + minId] = getModuleLocation(moduleNode);
200 }, {});
201}
202
203function getModuleLocation(node) {
204 return _.pick(node, 'start', 'end');
205}