webpack-bundle-analyzer 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -29
- package/lib/BundleAnalyzerPlugin.js +125 -37
- package/lib/Logger.js +70 -12
- package/lib/analyzer.js +248 -121
- package/lib/bin/analyzer.js +72 -51
- package/lib/index.js +2 -2
- package/lib/parseUtils.js +271 -162
- package/lib/sizeUtils.js +24 -10
- package/lib/statsUtils.js +38 -16
- package/lib/template.js +83 -36
- package/lib/tree/BaseFolder.js +84 -23
- package/lib/tree/ConcatenatedModule.js +78 -20
- package/lib/tree/ContentFolder.js +36 -7
- package/lib/tree/ContentModule.js +38 -9
- package/lib/tree/Folder.js +51 -15
- package/lib/tree/Module.js +67 -14
- package/lib/tree/Node.js +10 -3
- package/lib/tree/utils.js +14 -4
- package/lib/utils.js +52 -24
- package/lib/viewer.js +182 -78
- package/package.json +85 -76
- package/public/viewer.js +59 -8
- package/public/viewer.js.LICENSE.txt +20 -7
- package/public/viewer.js.map +1 -1
package/lib/parseUtils.js
CHANGED
|
@@ -1,42 +1,281 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
/** @typedef {import("acorn").Node} Node */
|
|
4
|
+
/** @typedef {import("acorn").CallExpression} CallExpression */
|
|
5
|
+
/** @typedef {import("acorn").ExpressionStatement} ExpressionStatement */
|
|
6
|
+
/** @typedef {import("acorn").Expression} Expression */
|
|
7
|
+
/** @typedef {import("acorn").SpreadElement} SpreadElement */
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const acorn = require("acorn");
|
|
11
|
+
const walk = require("acorn-walk");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Expression} node node
|
|
15
|
+
* @returns {boolean} true when id is numeric, otherwise false
|
|
16
|
+
*/
|
|
17
|
+
function isNumericId(node) {
|
|
18
|
+
return node.type === "Literal" && node.value !== null && node.value !== undefined && Number.isInteger(node.value) && /** @type {number} */node.value >= 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Expression | SpreadElement | null} node node
|
|
23
|
+
* @returns {boolean} true when module id, otherwise false
|
|
24
|
+
*/
|
|
25
|
+
function isModuleId(node) {
|
|
26
|
+
return node !== null && node.type === "Literal" && (isNumericId(node) || typeof node.value === "string");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Expression | SpreadElement} node node
|
|
31
|
+
* @returns {boolean} true when module wrapper, otherwise false
|
|
32
|
+
*/
|
|
33
|
+
function isModuleWrapper(node) {
|
|
34
|
+
return (
|
|
35
|
+
// It's an anonymous function expression that wraps module
|
|
36
|
+
(node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && !node.id ||
|
|
37
|
+
// If `DedupePlugin` is used it can be an ID of duplicated module...
|
|
38
|
+
isModuleId(node) ||
|
|
39
|
+
// or an array of shape [<module_id>, ...args]
|
|
40
|
+
node.type === "ArrayExpression" && node.elements.length > 1 && isModuleId(node.elements[0])
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Expression | SpreadElement | null} node node
|
|
46
|
+
* @returns {boolean} true when module hash, otherwise false
|
|
47
|
+
*/
|
|
48
|
+
function isModulesHash(node) {
|
|
49
|
+
return node !== null && node.type === "ObjectExpression" && node.properties.filter(property => property.type !== "SpreadElement").map(node => node.value).every(isModuleWrapper);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Expression | SpreadElement | null} node node
|
|
54
|
+
* @returns {boolean} true when module array, otherwise false
|
|
55
|
+
*/
|
|
56
|
+
function isModulesArray(node) {
|
|
57
|
+
return node !== null && node.type === "ArrayExpression" && node.elements.every(elem =>
|
|
58
|
+
// Some of array items may be skipped because there is no module with such id
|
|
59
|
+
!elem || isModuleWrapper(elem));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Expression | SpreadElement | null} node node
|
|
64
|
+
* @returns {boolean} true when simple modules list, otherwise false
|
|
65
|
+
*/
|
|
66
|
+
function isSimpleModulesList(node) {
|
|
67
|
+
return (
|
|
68
|
+
// Modules are contained in hash. Keys are module ids.
|
|
69
|
+
isModulesHash(node) ||
|
|
70
|
+
// Modules are contained in array. Indexes are module ids.
|
|
71
|
+
isModulesArray(node)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Expression | SpreadElement | null} node node
|
|
77
|
+
* @returns {boolean} true when optimized modules array, otherwise false
|
|
78
|
+
*/
|
|
79
|
+
function isOptimizedModulesArray(node) {
|
|
80
|
+
// Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
|
|
81
|
+
// https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
|
|
82
|
+
// The `<minimum ID>` + array indexes are module ids
|
|
83
|
+
return node !== null && node.type === "CallExpression" && node.callee.type === "MemberExpression" &&
|
|
84
|
+
// Make sure the object called is `Array(<some number>)`
|
|
85
|
+
node.callee.object.type === "CallExpression" && node.callee.object.callee.type === "Identifier" && node.callee.object.callee.name === "Array" && node.callee.object.arguments.length === 1 && node.callee.object.arguments[0].type !== "SpreadElement" && isNumericId(node.callee.object.arguments[0]) &&
|
|
86
|
+
// Make sure the property X called for `Array(<some number>).X` is `concat`
|
|
87
|
+
node.callee.property.type === "Identifier" && node.callee.property.name === "concat" &&
|
|
88
|
+
// Make sure exactly one array is passed in to `concat`
|
|
89
|
+
node.arguments.length === 1 && isModulesArray(node.arguments[0]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {Expression | SpreadElement | null} node node
|
|
94
|
+
* @returns {boolean} true when modules list, otherwise false
|
|
95
|
+
*/
|
|
96
|
+
function isModulesList(node) {
|
|
97
|
+
return isSimpleModulesList(node) ||
|
|
98
|
+
// Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
|
|
99
|
+
isOptimizedModulesArray(node);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @typedef {{ start: number, end: number }} Location */
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {Node} node node
|
|
106
|
+
* @returns {Location} location
|
|
107
|
+
*/
|
|
108
|
+
function getModuleLocation(node) {
|
|
109
|
+
return {
|
|
110
|
+
start: node.start,
|
|
111
|
+
end: node.end
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @typedef {Record<number, Location>} ModulesLocations */
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {Expression | SpreadElement} node node
|
|
119
|
+
* @returns {ModulesLocations} modules locations
|
|
120
|
+
*/
|
|
121
|
+
function getModulesLocations(node) {
|
|
122
|
+
if (node.type === "ObjectExpression") {
|
|
123
|
+
// Modules hash
|
|
124
|
+
const modulesNodes = node.properties;
|
|
125
|
+
return modulesNodes.reduce((result, moduleNode) => {
|
|
126
|
+
if (moduleNode.type !== "Property") {
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
const moduleId = moduleNode.key.type === "Identifier" ? moduleNode.key.name :
|
|
130
|
+
// @ts-expect-error need verify why we need it, tests not cover it case
|
|
131
|
+
moduleNode.key.value;
|
|
132
|
+
if (moduleId === "undefined") {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
result[moduleId] = getModuleLocation(moduleNode.value);
|
|
136
|
+
return result;
|
|
137
|
+
}, /** @type {ModulesLocations} */{});
|
|
138
|
+
}
|
|
139
|
+
const isOptimizedArray = node.type === "CallExpression";
|
|
140
|
+
if (node.type === "ArrayExpression" || isOptimizedArray) {
|
|
141
|
+
// Modules array or optimized array
|
|
142
|
+
const minId = isOptimizedArray && node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression" && node.callee.object.arguments[0].type === "Literal" ?
|
|
143
|
+
// Get the [minId] value from the Array() call first argument literal value
|
|
144
|
+
/** @type {number} */
|
|
145
|
+
node.callee.object.arguments[0].value :
|
|
146
|
+
// `0` for simple array
|
|
147
|
+
0;
|
|
148
|
+
const modulesNodes = isOptimizedArray ?
|
|
149
|
+
// The modules reside in the `concat()` function call arguments
|
|
150
|
+
node.arguments[0].type === "ArrayExpression" ? node.arguments[0].elements : [] : node.elements;
|
|
151
|
+
return modulesNodes.reduce((result, moduleNode, i) => {
|
|
152
|
+
if (moduleNode) {
|
|
153
|
+
result[i + minId] = getModuleLocation(moduleNode);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}, /** @type {ModulesLocations} */{});
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {ExpressionStatement} node node
|
|
163
|
+
* @returns {boolean} true when IIFE, otherwise false
|
|
164
|
+
*/
|
|
165
|
+
function isIIFE(node) {
|
|
166
|
+
return node.type === "ExpressionStatement" && (node.expression.type === "CallExpression" || node.expression.type === "UnaryExpression" && node.expression.argument.type === "CallExpression");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {ExpressionStatement} node node
|
|
171
|
+
* @returns {Expression} IIFE call expression
|
|
172
|
+
*/
|
|
173
|
+
function getIIFECallExpression(node) {
|
|
174
|
+
if (node.expression.type === "UnaryExpression") {
|
|
175
|
+
return node.expression.argument;
|
|
176
|
+
}
|
|
177
|
+
return node.expression;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {Expression} node node
|
|
182
|
+
* @returns {boolean} true when chunks ids, otherwose false
|
|
183
|
+
*/
|
|
184
|
+
function isChunkIds(node) {
|
|
185
|
+
// Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
|
|
186
|
+
return node.type === "ArrayExpression" && node.elements.every(isModuleId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {(Expression | SpreadElement | null)[]} args arguments
|
|
191
|
+
* @returns {boolean} true when async chunk arguments, otherwise false
|
|
192
|
+
*/
|
|
193
|
+
function mayBeAsyncChunkArguments(args) {
|
|
194
|
+
return args.length >= 2 && args[0] !== null && args[0].type !== "SpreadElement" && isChunkIds(args[0]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns bundle source except modules
|
|
199
|
+
* @param {string} content content
|
|
200
|
+
* @param {ModulesLocations | null} modulesLocations modules locations
|
|
201
|
+
* @returns {string} runtime code
|
|
202
|
+
*/
|
|
203
|
+
function getBundleRuntime(content, modulesLocations) {
|
|
204
|
+
const sortedLocations = Object.values(modulesLocations || {}).toSorted((a, b) => a.start - b.start);
|
|
205
|
+
let result = "";
|
|
206
|
+
let lastIndex = 0;
|
|
207
|
+
for (const {
|
|
208
|
+
start,
|
|
209
|
+
end
|
|
210
|
+
} of sortedLocations) {
|
|
211
|
+
result += content.slice(lastIndex, start);
|
|
212
|
+
lastIndex = end;
|
|
213
|
+
}
|
|
214
|
+
return result + content.slice(lastIndex);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {CallExpression} node node
|
|
219
|
+
* @returns {boolean} true when is async chunk push expression, otheriwse false
|
|
220
|
+
*/
|
|
221
|
+
function isAsyncChunkPushExpression(node) {
|
|
222
|
+
const {
|
|
223
|
+
callee,
|
|
224
|
+
arguments: args
|
|
225
|
+
} = node;
|
|
226
|
+
return callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "push" && callee.object.type === "AssignmentExpression" && args.length === 1 && args[0].type === "ArrayExpression" && mayBeAsyncChunkArguments(args[0].elements) && isModulesList(args[0].elements[1]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {CallExpression} node node
|
|
231
|
+
* @returns {boolean} true when is async web worker, otherwise false
|
|
232
|
+
*/
|
|
233
|
+
function isAsyncWebWorkerChunkExpression(node) {
|
|
234
|
+
const {
|
|
235
|
+
callee,
|
|
236
|
+
type,
|
|
237
|
+
arguments: args
|
|
238
|
+
} = node;
|
|
239
|
+
return type === "CallExpression" && callee.type === "MemberExpression" && args.length === 2 && args[0].type !== "SpreadElement" && isChunkIds(args[0]) && isModulesList(args[1]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @typedef {Record<string, string>} Modules */
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} bundlePath bundle path
|
|
246
|
+
* @param {{ sourceType: "script" | "module" }} opts options
|
|
247
|
+
* @returns {{ modules: Modules, src: string, runtimeSrc: string }} parsed result
|
|
248
|
+
*/
|
|
249
|
+
module.exports.parseBundle = function parseBundle(bundlePath, opts) {
|
|
10
250
|
const {
|
|
11
|
-
sourceType =
|
|
251
|
+
sourceType = "script"
|
|
12
252
|
} = opts || {};
|
|
13
|
-
const content = fs.readFileSync(bundlePath,
|
|
253
|
+
const content = fs.readFileSync(bundlePath, "utf8");
|
|
14
254
|
const ast = acorn.parse(content, {
|
|
15
255
|
sourceType,
|
|
16
|
-
|
|
17
|
-
// Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
|
|
18
|
-
// Seems like `acorn` supports such weird option value.
|
|
19
|
-
ecmaVersion: 2050
|
|
256
|
+
ecmaVersion: "latest"
|
|
20
257
|
});
|
|
258
|
+
|
|
259
|
+
/** @type {{ locations: ModulesLocations | null, expressionStatementDepth: number }} */
|
|
21
260
|
const walkState = {
|
|
22
261
|
locations: null,
|
|
23
262
|
expressionStatementDepth: 0
|
|
24
263
|
};
|
|
25
264
|
walk.recursive(ast, walkState, {
|
|
26
|
-
ExpressionStatement(node, state,
|
|
265
|
+
ExpressionStatement(node, state, callback) {
|
|
27
266
|
if (state.locations) return;
|
|
28
267
|
state.expressionStatementDepth++;
|
|
29
268
|
if (
|
|
30
269
|
// Webpack 5 stores modules in the the top-level IIFE
|
|
31
270
|
state.expressionStatementDepth === 1 && ast.body.includes(node) && isIIFE(node)) {
|
|
32
271
|
const fn = getIIFECallExpression(node);
|
|
33
|
-
if (
|
|
272
|
+
if (fn.type === "CallExpression" &&
|
|
34
273
|
// It should not contain neither arguments
|
|
35
|
-
fn.arguments.length === 0 &&
|
|
274
|
+
fn.arguments.length === 0 && (fn.callee.type === "FunctionExpression" || fn.callee.type === "ArrowFunctionExpression") &&
|
|
36
275
|
// ...nor parameters
|
|
37
|
-
fn.callee.params.length === 0) {
|
|
276
|
+
fn.callee.params.length === 0 && fn.callee.body.type === "BlockStatement") {
|
|
38
277
|
// Modules are stored in the very first variable declaration as hash
|
|
39
|
-
const firstVariableDeclaration = fn.callee.body.body.find(node => node.type ===
|
|
278
|
+
const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === "VariableDeclaration");
|
|
40
279
|
if (firstVariableDeclaration) {
|
|
41
280
|
for (const declaration of firstVariableDeclaration.declarations) {
|
|
42
281
|
if (declaration.init && isModulesList(declaration.init)) {
|
|
@@ -50,7 +289,7 @@ function parseBundle(bundlePath, opts) {
|
|
|
50
289
|
}
|
|
51
290
|
}
|
|
52
291
|
if (!state.locations) {
|
|
53
|
-
|
|
292
|
+
callback(node.expression, state);
|
|
54
293
|
}
|
|
55
294
|
state.expressionStatementDepth--;
|
|
56
295
|
},
|
|
@@ -63,18 +302,18 @@ function parseBundle(bundlePath, opts) {
|
|
|
63
302
|
left,
|
|
64
303
|
right
|
|
65
304
|
} = node;
|
|
66
|
-
if (left && left.object && left.object.name ===
|
|
305
|
+
if (left && left.type === "MemberExpression" && left.object && left.object.type === "Identifier" && left.object.name === "exports" && left.property && left.property.type === "Identifier" && left.property.name === "modules" && isModulesHash(right)) {
|
|
67
306
|
state.locations = getModulesLocations(right);
|
|
68
307
|
}
|
|
69
308
|
},
|
|
70
|
-
CallExpression(node, state,
|
|
309
|
+
CallExpression(node, state, callback) {
|
|
71
310
|
if (state.locations) return;
|
|
72
311
|
const args = node.arguments;
|
|
73
312
|
|
|
74
313
|
// Main chunk with webpack loader.
|
|
75
314
|
// Modules are stored in first argument:
|
|
76
315
|
// (function (...) {...})(<modules>)
|
|
77
|
-
if (node.callee.type ===
|
|
316
|
+
if (node.callee.type === "FunctionExpression" && !node.callee.id && args.length === 1 && isSimpleModulesList(args[0])) {
|
|
78
317
|
state.locations = getModulesLocations(args[0]);
|
|
79
318
|
return;
|
|
80
319
|
}
|
|
@@ -82,7 +321,7 @@ function parseBundle(bundlePath, opts) {
|
|
|
82
321
|
// Async Webpack < v4 chunk without webpack loader.
|
|
83
322
|
// webpackJsonp([<chunks>], <modules>, ...)
|
|
84
323
|
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
|
|
85
|
-
if (node.callee.type ===
|
|
324
|
+
if (node.callee.type === "Identifier" && mayBeAsyncChunkArguments(args) && args[1].type !== "SpreadElement" && isModulesList(args[1])) {
|
|
86
325
|
state.locations = getModulesLocations(args[1]);
|
|
87
326
|
return;
|
|
88
327
|
}
|
|
@@ -90,7 +329,7 @@ function parseBundle(bundlePath, opts) {
|
|
|
90
329
|
// Async Webpack v4 chunk without webpack loader.
|
|
91
330
|
// (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
|
|
92
331
|
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
|
|
93
|
-
if (isAsyncChunkPushExpression(node)) {
|
|
332
|
+
if (isAsyncChunkPushExpression(node) && args[0].type === "ArrayExpression" && args[0].elements[1]) {
|
|
94
333
|
state.locations = getModulesLocations(args[0].elements[1]);
|
|
95
334
|
return;
|
|
96
335
|
}
|
|
@@ -105,152 +344,22 @@ function parseBundle(bundlePath, opts) {
|
|
|
105
344
|
|
|
106
345
|
// Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
|
|
107
346
|
// features (e.g. `umd` library output) can wrap modules list into additional IIFE.
|
|
108
|
-
|
|
347
|
+
for (const arg of args) {
|
|
348
|
+
callback(arg, state);
|
|
349
|
+
}
|
|
109
350
|
}
|
|
110
351
|
});
|
|
352
|
+
|
|
353
|
+
/** @type {Modules} */
|
|
111
354
|
const modules = {};
|
|
112
355
|
if (walkState.locations) {
|
|
113
|
-
Object.entries(walkState.locations)
|
|
356
|
+
for (const [id, loc] of Object.entries(walkState.locations)) {
|
|
114
357
|
modules[id] = content.slice(loc.start, loc.end);
|
|
115
|
-
}
|
|
358
|
+
}
|
|
116
359
|
}
|
|
117
360
|
return {
|
|
118
361
|
modules,
|
|
119
362
|
src: content,
|
|
120
363
|
runtimeSrc: getBundleRuntime(content, walkState.locations)
|
|
121
364
|
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Returns bundle source except modules
|
|
126
|
-
*/
|
|
127
|
-
function getBundleRuntime(content, modulesLocations) {
|
|
128
|
-
const sortedLocations = Object.values(modulesLocations || {}).sort((a, b) => a.start - b.start);
|
|
129
|
-
let result = '';
|
|
130
|
-
let lastIndex = 0;
|
|
131
|
-
for (const {
|
|
132
|
-
start,
|
|
133
|
-
end
|
|
134
|
-
} of sortedLocations) {
|
|
135
|
-
result += content.slice(lastIndex, start);
|
|
136
|
-
lastIndex = end;
|
|
137
|
-
}
|
|
138
|
-
return result + content.slice(lastIndex, content.length);
|
|
139
|
-
}
|
|
140
|
-
function isIIFE(node) {
|
|
141
|
-
return node.type === 'ExpressionStatement' && (node.expression.type === 'CallExpression' || node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression');
|
|
142
|
-
}
|
|
143
|
-
function getIIFECallExpression(node) {
|
|
144
|
-
if (node.expression.type === 'UnaryExpression') {
|
|
145
|
-
return node.expression.argument;
|
|
146
|
-
} else {
|
|
147
|
-
return node.expression;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function isModulesList(node) {
|
|
151
|
-
return isSimpleModulesList(node) ||
|
|
152
|
-
// Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
|
|
153
|
-
isOptimizedModulesArray(node);
|
|
154
|
-
}
|
|
155
|
-
function isSimpleModulesList(node) {
|
|
156
|
-
return (
|
|
157
|
-
// Modules are contained in hash. Keys are module ids.
|
|
158
|
-
isModulesHash(node) ||
|
|
159
|
-
// Modules are contained in array. Indexes are module ids.
|
|
160
|
-
isModulesArray(node)
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
function isModulesHash(node) {
|
|
164
|
-
return node.type === 'ObjectExpression' && node.properties.map(node => node.value).every(isModuleWrapper);
|
|
165
|
-
}
|
|
166
|
-
function isModulesArray(node) {
|
|
167
|
-
return node.type === 'ArrayExpression' && node.elements.every(elem =>
|
|
168
|
-
// Some of array items may be skipped because there is no module with such id
|
|
169
|
-
!elem || isModuleWrapper(elem));
|
|
170
|
-
}
|
|
171
|
-
function isOptimizedModulesArray(node) {
|
|
172
|
-
// Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
|
|
173
|
-
// https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
|
|
174
|
-
// The `<minimum ID>` + array indexes are module ids
|
|
175
|
-
return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' &&
|
|
176
|
-
// Make sure the object called is `Array(<some number>)`
|
|
177
|
-
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]) &&
|
|
178
|
-
// Make sure the property X called for `Array(<some number>).X` is `concat`
|
|
179
|
-
node.callee.property.type === 'Identifier' && node.callee.property.name === 'concat' &&
|
|
180
|
-
// Make sure exactly one array is passed in to `concat`
|
|
181
|
-
node.arguments.length === 1 && isModulesArray(node.arguments[0]);
|
|
182
|
-
}
|
|
183
|
-
function isModuleWrapper(node) {
|
|
184
|
-
return (
|
|
185
|
-
// It's an anonymous function expression that wraps module
|
|
186
|
-
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id ||
|
|
187
|
-
// If `DedupePlugin` is used it can be an ID of duplicated module...
|
|
188
|
-
isModuleId(node) ||
|
|
189
|
-
// or an array of shape [<module_id>, ...args]
|
|
190
|
-
node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0])
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
function isModuleId(node) {
|
|
194
|
-
return node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string');
|
|
195
|
-
}
|
|
196
|
-
function isNumericId(node) {
|
|
197
|
-
return node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0;
|
|
198
|
-
}
|
|
199
|
-
function isChunkIds(node) {
|
|
200
|
-
// Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
|
|
201
|
-
return node.type === 'ArrayExpression' && node.elements.every(isModuleId);
|
|
202
|
-
}
|
|
203
|
-
function isAsyncChunkPushExpression(node) {
|
|
204
|
-
const {
|
|
205
|
-
callee,
|
|
206
|
-
arguments: args
|
|
207
|
-
} = node;
|
|
208
|
-
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]);
|
|
209
|
-
}
|
|
210
|
-
function mayBeAsyncChunkArguments(args) {
|
|
211
|
-
return args.length >= 2 && isChunkIds(args[0]);
|
|
212
|
-
}
|
|
213
|
-
function isAsyncWebWorkerChunkExpression(node) {
|
|
214
|
-
const {
|
|
215
|
-
callee,
|
|
216
|
-
type,
|
|
217
|
-
arguments: args
|
|
218
|
-
} = node;
|
|
219
|
-
return type === 'CallExpression' && callee.type === 'MemberExpression' && args.length === 2 && isChunkIds(args[0]) && isModulesList(args[1]);
|
|
220
|
-
}
|
|
221
|
-
function getModulesLocations(node) {
|
|
222
|
-
if (node.type === 'ObjectExpression') {
|
|
223
|
-
// Modules hash
|
|
224
|
-
const modulesNodes = node.properties;
|
|
225
|
-
return modulesNodes.reduce((result, moduleNode) => {
|
|
226
|
-
const moduleId = moduleNode.key.name || moduleNode.key.value;
|
|
227
|
-
result[moduleId] = getModuleLocation(moduleNode.value);
|
|
228
|
-
return result;
|
|
229
|
-
}, {});
|
|
230
|
-
}
|
|
231
|
-
const isOptimizedArray = node.type === 'CallExpression';
|
|
232
|
-
if (node.type === 'ArrayExpression' || isOptimizedArray) {
|
|
233
|
-
// Modules array or optimized array
|
|
234
|
-
const minId = isOptimizedArray ?
|
|
235
|
-
// Get the [minId] value from the Array() call first argument literal value
|
|
236
|
-
node.callee.object.arguments[0].value :
|
|
237
|
-
// `0` for simple array
|
|
238
|
-
0;
|
|
239
|
-
const modulesNodes = isOptimizedArray ?
|
|
240
|
-
// The modules reside in the `concat()` function call arguments
|
|
241
|
-
node.arguments[0].elements : node.elements;
|
|
242
|
-
return modulesNodes.reduce((result, moduleNode, i) => {
|
|
243
|
-
if (moduleNode) {
|
|
244
|
-
result[i + minId] = getModuleLocation(moduleNode);
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
}, {});
|
|
248
|
-
}
|
|
249
|
-
return {};
|
|
250
|
-
}
|
|
251
|
-
function getModuleLocation(node) {
|
|
252
|
-
return {
|
|
253
|
-
start: node.start,
|
|
254
|
-
end: node.end
|
|
255
|
-
};
|
|
256
|
-
}
|
|
365
|
+
};
|
package/lib/sizeUtils.js
CHANGED
|
@@ -5,15 +5,29 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.getCompressedSize = getCompressedSize;
|
|
7
7
|
exports.isZstdSupported = void 0;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
var _nodeZlib = _interopRequireDefault(require("node:zlib"));
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
const isZstdSupported = exports.isZstdSupported = "createZstdCompress" in _nodeZlib.default;
|
|
11
|
+
|
|
12
|
+
/** @typedef {"gzip" | "brotli" | "zstd"} Algorithm */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Algorithm} algorithm compression algorithm
|
|
16
|
+
* @param {string} input input
|
|
17
|
+
* @returns {number} compressed size
|
|
18
|
+
*/
|
|
19
|
+
function getCompressedSize(algorithm, input) {
|
|
20
|
+
if (algorithm === "gzip") {
|
|
21
|
+
return _nodeZlib.default.gzipSync(input, {
|
|
22
|
+
level: 9
|
|
23
|
+
}).length;
|
|
24
|
+
}
|
|
25
|
+
if (algorithm === "brotli") {
|
|
26
|
+
return _nodeZlib.default.brotliCompressSync(input).length;
|
|
27
|
+
}
|
|
28
|
+
if (algorithm === "zstd" && isZstdSupported) {
|
|
29
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
30
|
+
return _nodeZlib.default.zstdCompressSync(input).length;
|
|
17
31
|
}
|
|
18
|
-
throw new Error(`Unsupported compression algorithm: ${
|
|
32
|
+
throw new Error(`Unsupported compression algorithm: ${algorithm}.`);
|
|
19
33
|
}
|
package/lib/statsUtils.js
CHANGED
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
createWriteStream
|
|
5
|
-
} = require(
|
|
5
|
+
} = require("node:fs");
|
|
6
6
|
const {
|
|
7
7
|
Readable
|
|
8
|
-
} = require(
|
|
8
|
+
} = require("node:stream");
|
|
9
|
+
const {
|
|
10
|
+
pipeline
|
|
11
|
+
} = require("node:stream/promises");
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("./BundleAnalyzerPlugin").EXPECTED_ANY} EXPECTED_ANY */
|
|
14
|
+
/** @typedef {import("webpack").StatsCompilation} StatsCompilation */
|
|
15
|
+
|
|
9
16
|
class StatsSerializeStream extends Readable {
|
|
17
|
+
/**
|
|
18
|
+
* @param {StatsCompilation} stats stats
|
|
19
|
+
*/
|
|
10
20
|
constructor(stats) {
|
|
11
21
|
super();
|
|
12
22
|
this._indentLevel = 0;
|
|
13
23
|
this._stringifier = this._stringify(stats);
|
|
14
24
|
}
|
|
15
25
|
get _indent() {
|
|
16
|
-
return
|
|
26
|
+
return " ".repeat(this._indentLevel);
|
|
17
27
|
}
|
|
18
28
|
_read() {
|
|
19
29
|
let readMore = true;
|
|
@@ -30,25 +40,31 @@ class StatsSerializeStream extends Readable {
|
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {EXPECTED_ANY} obj obj
|
|
46
|
+
* @returns {Generator<string, undefined, unknown>} stringified result
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
33
49
|
*_stringify(obj) {
|
|
34
|
-
if (typeof obj ===
|
|
50
|
+
if (typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean" || obj === null) {
|
|
35
51
|
yield JSON.stringify(obj);
|
|
36
52
|
} else if (Array.isArray(obj)) {
|
|
37
|
-
yield
|
|
53
|
+
yield "[";
|
|
38
54
|
this._indentLevel++;
|
|
39
55
|
let isFirst = true;
|
|
40
56
|
for (let item of obj) {
|
|
41
57
|
if (item === undefined) {
|
|
42
58
|
item = null;
|
|
43
59
|
}
|
|
44
|
-
yield `${isFirst ?
|
|
60
|
+
yield `${isFirst ? "" : ","}\n${this._indent}`;
|
|
45
61
|
yield* this._stringify(item);
|
|
46
62
|
isFirst = false;
|
|
47
63
|
}
|
|
48
64
|
this._indentLevel--;
|
|
49
|
-
yield obj.length ? `\n${this._indent}]` :
|
|
65
|
+
yield obj.length ? `\n${this._indent}]` : "]";
|
|
50
66
|
} else {
|
|
51
|
-
yield
|
|
67
|
+
yield "{";
|
|
52
68
|
this._indentLevel++;
|
|
53
69
|
let isFirst = true;
|
|
54
70
|
const entries = Object.entries(obj);
|
|
@@ -56,19 +72,25 @@ class StatsSerializeStream extends Readable {
|
|
|
56
72
|
if (itemValue === undefined) {
|
|
57
73
|
continue;
|
|
58
74
|
}
|
|
59
|
-
yield `${isFirst ?
|
|
75
|
+
yield `${isFirst ? "" : ","}\n${this._indent}${JSON.stringify(itemKey)}: `;
|
|
60
76
|
yield* this._stringify(itemValue);
|
|
61
77
|
isFirst = false;
|
|
62
78
|
}
|
|
63
79
|
this._indentLevel--;
|
|
64
|
-
yield entries.length ? `\n${this._indent}}` :
|
|
80
|
+
yield entries.length ? `\n${this._indent}}` : "}";
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {StatsCompilation} stats stats
|
|
87
|
+
* @param {string} filepath filepath file path
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
70
90
|
async function writeStats(stats, filepath) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
await pipeline(new StatsSerializeStream(stats), createWriteStream(filepath));
|
|
92
|
+
}
|
|
93
|
+
module.exports = {
|
|
94
|
+
StatsSerializeStream,
|
|
95
|
+
writeStats
|
|
96
|
+
};
|