webpack-bundle-analyzer 5.1.1 → 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 +34 -32
- package/lib/BundleAnalyzerPlugin.js +125 -37
- package/lib/Logger.js +70 -12
- package/lib/analyzer.js +249 -120
- package/lib/bin/analyzer.js +74 -50
- package/lib/index.js +2 -2
- package/lib/parseUtils.js +271 -162
- package/lib/sizeUtils.js +26 -7
- package/lib/statsUtils.js +38 -16
- package/lib/template.js +83 -36
- package/lib/tree/BaseFolder.js +84 -23
- package/lib/tree/ConcatenatedModule.js +80 -19
- package/lib/tree/ContentFolder.js +39 -6
- package/lib/tree/ContentModule.js +40 -8
- package/lib/tree/Folder.js +55 -15
- package/lib/tree/Module.js +74 -13
- 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 +87 -78
- package/public/viewer.js +59 -8
- package/public/viewer.js.LICENSE.txt +20 -7
- package/public/viewer.js.map +1 -1
package/lib/analyzer.js
CHANGED
|
@@ -1,74 +1,254 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
5
|
const {
|
|
6
6
|
parseChunked
|
|
7
|
-
} = require(
|
|
8
|
-
const Logger = require(
|
|
9
|
-
const Folder = require('./tree/Folder').default;
|
|
7
|
+
} = require("@discoveryjs/json-ext");
|
|
8
|
+
const Logger = require("./Logger");
|
|
10
9
|
const {
|
|
11
10
|
parseBundle
|
|
12
|
-
} = require(
|
|
13
|
-
const {
|
|
14
|
-
createAssetsFilter
|
|
15
|
-
} = require('./utils');
|
|
11
|
+
} = require("./parseUtils");
|
|
16
12
|
const {
|
|
17
13
|
getCompressedSize
|
|
18
|
-
} = require(
|
|
14
|
+
} = require("./sizeUtils");
|
|
15
|
+
const Folder = require("./tree/Folder").default;
|
|
16
|
+
const {
|
|
17
|
+
createAssetsFilter
|
|
18
|
+
} = require("./utils");
|
|
19
19
|
const FILENAME_QUERY_REGEXP = /\?.*$/u;
|
|
20
20
|
const FILENAME_EXTENSIONS = /\.(js|mjs|cjs|bundle)$/iu;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
21
|
+
|
|
22
|
+
/** @typedef {import("webpack").StatsCompilation} StatsCompilation */
|
|
23
|
+
/** @typedef {import("webpack").StatsModule} StatsModule */
|
|
24
|
+
/** @typedef {import("webpack").StatsAsset} StatsAsset */
|
|
25
|
+
/** @typedef {import("./BundleAnalyzerPlugin").CompressionAlgorithm} CompressionAlgorithm */
|
|
26
|
+
/** @typedef {import("./BundleAnalyzerPlugin").ExcludeAssets} ExcludeAssets */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} AnalyzerOptions
|
|
30
|
+
* @property {"gzip" | "brotli" | "zstd"} compressionAlgorithm compression algorithm
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {StatsModule[]} modules modules
|
|
35
|
+
* @param {AnalyzerOptions} options options
|
|
36
|
+
* @returns {Folder} a folder class
|
|
37
|
+
*/
|
|
38
|
+
function createModulesTree(modules, options) {
|
|
39
|
+
const root = new Folder(".", options);
|
|
40
|
+
for (const module of modules) {
|
|
41
|
+
root.addModule(module);
|
|
42
|
+
}
|
|
43
|
+
root.mergeNestedFolders();
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* arr-flatten <https://github.com/jonschlinkert/arr-flatten>
|
|
49
|
+
*
|
|
50
|
+
* Copyright (c) 2014-2017, Jon Schlinkert.
|
|
51
|
+
* Released under the MIT License.
|
|
52
|
+
*
|
|
53
|
+
* Modified by Sukka <https://skk.moe>
|
|
54
|
+
*
|
|
55
|
+
* Replace recursively flatten with one-level deep flatten to match lodash.flatten
|
|
56
|
+
*
|
|
57
|
+
* TODO: replace with Array.prototype.flat once Node.js 10 support is dropped
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Flattens an array by one level.
|
|
61
|
+
* @template T
|
|
62
|
+
* @param {(T | T[])[]} arr the array to flatten
|
|
63
|
+
* @returns {T[]} a new array containing the flattened elements
|
|
64
|
+
*/
|
|
65
|
+
function flatten(arr) {
|
|
66
|
+
if (!arr) return [];
|
|
67
|
+
const len = arr.length;
|
|
68
|
+
if (!len) return [];
|
|
69
|
+
let cur;
|
|
70
|
+
const res = [];
|
|
71
|
+
for (let i = 0; i < len; i++) {
|
|
72
|
+
cur = arr[i];
|
|
73
|
+
if (Array.isArray(cur)) {
|
|
74
|
+
res.push(...cur);
|
|
75
|
+
} else {
|
|
76
|
+
res.push(cur);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return res;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {StatsCompilation} bundleStats bundle stats
|
|
84
|
+
* @param {string} assetName asset name
|
|
85
|
+
* @returns {boolean} child asset bundlers
|
|
86
|
+
*/
|
|
87
|
+
function getChildAssetBundles(bundleStats, assetName) {
|
|
88
|
+
return flatten((bundleStats.children || (/** @type {StatsCompilation} */[])).find(
|
|
89
|
+
/**
|
|
90
|
+
* @param {StatsCompilation} child child stats
|
|
91
|
+
* @returns {string[][]} assets by chunk name
|
|
92
|
+
*/
|
|
93
|
+
child => Object.values(child.assetsByChunkName || []))).includes(assetName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {StatsAsset} statsAsset stats asset
|
|
98
|
+
* @param {StatsModule} statsModule stats modules
|
|
99
|
+
* @returns {boolean} true when asset has a module
|
|
100
|
+
*/
|
|
101
|
+
function assetHasModule(statsAsset, statsModule) {
|
|
102
|
+
// Checking if this module is the part of asset chunks
|
|
103
|
+
return (statsModule.chunks || []).some(moduleChunk => statsAsset.chunks && statsAsset.chunks.includes(moduleChunk));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {StatsModule} statsModule stats Module
|
|
108
|
+
* @returns {boolean} true when runtime modules, otherwise false
|
|
109
|
+
*/
|
|
110
|
+
function isRuntimeModule(statsModule) {
|
|
111
|
+
return statsModule.moduleType === "runtime";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {StatsCompilation} bundleStats bundle stats
|
|
116
|
+
* @returns {StatsModule[]} modules
|
|
117
|
+
*/
|
|
118
|
+
function getBundleModules(bundleStats) {
|
|
119
|
+
/** @type {Set<string | number>} */
|
|
120
|
+
const seenIds = new Set();
|
|
121
|
+
const modules = /** @type {StatsModule[]} */[...(bundleStats.chunks?.map(chunk => chunk.modules) || []), ...(bundleStats.modules || [])].filter(Boolean);
|
|
122
|
+
return flatten(modules).filter(mod => {
|
|
123
|
+
// Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
|
|
124
|
+
if (isRuntimeModule(mod)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (seenIds.has(mod.id)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
seenIds.add(mod.id);
|
|
131
|
+
return true;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @typedef {Record<string, Record<string, boolean>>} ChunkToInitialByEntrypoint */
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {StatsCompilation} bundleStats bundle stats
|
|
139
|
+
* @returns {ChunkToInitialByEntrypoint} chunk to initial by entrypoint
|
|
140
|
+
*/
|
|
141
|
+
function getChunkToInitialByEntrypoint(bundleStats) {
|
|
142
|
+
if (bundleStats === null || bundleStats === undefined) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
/** @type {ChunkToInitialByEntrypoint} */
|
|
146
|
+
const chunkToEntrypointInititalMap = {};
|
|
147
|
+
for (const entrypoint of Object.values(bundleStats.entrypoints || {})) {
|
|
148
|
+
for (const asset of entrypoint.assets || []) {
|
|
149
|
+
chunkToEntrypointInititalMap[asset.name] ??= {};
|
|
150
|
+
chunkToEntrypointInititalMap[asset.name][(/** @type {string} */
|
|
151
|
+
entrypoint.name)] = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return chunkToEntrypointInititalMap;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {StatsModule} statsModule stats modules
|
|
159
|
+
* @returns {boolean} true when entry module, otherwise false
|
|
160
|
+
*/
|
|
161
|
+
function isEntryModule(statsModule) {
|
|
162
|
+
return statsModule.depth === 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @typedef {object} ViewerDataOptions
|
|
167
|
+
* @property {Logger} logger logger
|
|
168
|
+
* @property {CompressionAlgorithm} compressionAlgorithm compression algorithm
|
|
169
|
+
* @property {ExcludeAssets} excludeAssets exclude assets
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/** @typedef {import("./tree/Module").ModuleChartData} ModuleChartData */
|
|
173
|
+
/** @typedef {import("./tree/ContentModule").ContentModuleChartData} ContentModuleChartData */
|
|
174
|
+
/** @typedef {import("./tree/ConcatenatedModule").ConcatenatedModuleChartData} ConcatenatedModuleChartData */
|
|
175
|
+
/** @typedef {import("./tree/ContentFolder").ContentFolderChartData} ContentFolderChartData */
|
|
176
|
+
/** @typedef {import("./tree/Folder").FolderChartData} FolderChartData */
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @typedef {object} ChartDataItem
|
|
180
|
+
* @property {string} label label
|
|
181
|
+
* @property {true} isAsset true when is asset, otherwise false
|
|
182
|
+
* @property {number} statSize stat size
|
|
183
|
+
* @property {number | undefined} parsedSize stat size
|
|
184
|
+
* @property {number | undefined} gzipSize gzip size
|
|
185
|
+
* @property {number | undefined} brotliSize brotli size
|
|
186
|
+
* @property {number | undefined} zstdSize zstd size
|
|
187
|
+
* @property {(ModuleChartData | ContentModuleChartData | ConcatenatedModuleChartData | ContentFolderChartData | FolderChartData)[]} groups groups
|
|
188
|
+
* @property {Record<string, boolean>} isInitialByEntrypoint record with initial entrypoints
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @typedef {ChartDataItem[]} ChartData
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {StatsCompilation} bundleStats bundle stats
|
|
197
|
+
* @param {string | null} bundleDir bundle dir
|
|
198
|
+
* @param {ViewerDataOptions=} opts options
|
|
199
|
+
* @returns {ChartData} chart data
|
|
200
|
+
*/
|
|
25
201
|
function getViewerData(bundleStats, bundleDir, opts) {
|
|
26
202
|
const {
|
|
27
203
|
logger = new Logger(),
|
|
28
|
-
compressionAlgorithm,
|
|
204
|
+
compressionAlgorithm = "gzip",
|
|
29
205
|
excludeAssets = null
|
|
30
206
|
} = opts || {};
|
|
31
207
|
const isAssetIncluded = createAssetsFilter(excludeAssets);
|
|
32
208
|
|
|
33
209
|
// Sometimes all the information is located in `children` array (e.g. problem in #10)
|
|
34
|
-
if ((bundleStats.assets
|
|
210
|
+
if ((bundleStats.assets === null || bundleStats.assets === undefined || bundleStats.assets.length === 0) && bundleStats.children && bundleStats.children.length > 0) {
|
|
35
211
|
const {
|
|
36
212
|
children
|
|
37
213
|
} = bundleStats;
|
|
38
|
-
bundleStats = bundleStats.children
|
|
214
|
+
[bundleStats] = bundleStats.children;
|
|
39
215
|
// Sometimes if there are additional child chunks produced add them as child assets,
|
|
40
216
|
// leave the 1st one as that is considered the 'root' asset.
|
|
41
217
|
for (let i = 1; i < children.length; i++) {
|
|
42
|
-
children[i].assets
|
|
218
|
+
for (const asset of children[i].assets || []) {
|
|
43
219
|
asset.isChild = true;
|
|
220
|
+
/** @type {StatsAsset[]} */
|
|
44
221
|
bundleStats.assets.push(asset);
|
|
45
|
-
}
|
|
222
|
+
}
|
|
46
223
|
}
|
|
47
224
|
} else if (bundleStats.children && bundleStats.children.length > 0) {
|
|
48
225
|
// Sometimes if there are additional child chunks produced add them as child assets
|
|
49
|
-
bundleStats.children
|
|
50
|
-
child.assets
|
|
226
|
+
for (const child of bundleStats.children) {
|
|
227
|
+
for (const asset of child.assets || []) {
|
|
51
228
|
asset.isChild = true;
|
|
229
|
+
/** @type {StatsAsset[]} */
|
|
52
230
|
bundleStats.assets.push(asset);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
55
233
|
}
|
|
56
234
|
|
|
57
235
|
// Picking only `*.js, *.cjs or *.mjs` assets from bundle that has non-empty `chunks` array
|
|
58
236
|
bundleStats.assets = (bundleStats.assets || []).filter(asset => {
|
|
59
237
|
// Filter out non 'asset' type asset if type is provided (Webpack 5 add a type to indicate asset types)
|
|
60
|
-
if (asset.type && asset.type !==
|
|
238
|
+
if (asset.type && asset.type !== "asset") {
|
|
61
239
|
return false;
|
|
62
240
|
}
|
|
63
241
|
|
|
64
242
|
// Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
|
|
65
243
|
// See #22
|
|
66
|
-
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP,
|
|
67
|
-
return FILENAME_EXTENSIONS.test(asset.name) && asset.chunks.length > 0 && isAssetIncluded(asset.name);
|
|
244
|
+
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, "");
|
|
245
|
+
return FILENAME_EXTENSIONS.test(asset.name) && asset.chunks && asset.chunks.length > 0 && isAssetIncluded(asset.name);
|
|
68
246
|
});
|
|
69
247
|
|
|
70
248
|
// Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
|
|
249
|
+
/** @type {Record<string, { src: string, runtimeSrc: string }> | null} */
|
|
71
250
|
let bundlesSources = null;
|
|
251
|
+
/** @type {Record<string | number, boolean> | null} */
|
|
72
252
|
let parsedModules = null;
|
|
73
253
|
if (bundleDir) {
|
|
74
254
|
bundlesSources = {};
|
|
@@ -78,10 +258,10 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
78
258
|
let bundleInfo;
|
|
79
259
|
try {
|
|
80
260
|
bundleInfo = parseBundle(assetFile, {
|
|
81
|
-
sourceType: statAsset.info.javascriptModule ?
|
|
261
|
+
sourceType: statAsset.info.javascriptModule ? "module" : "script"
|
|
82
262
|
});
|
|
83
263
|
} catch (err) {
|
|
84
|
-
const msg = err.code ===
|
|
264
|
+
const msg = /** @type {NodeJS.ErrnoException} */err.code === "ENOENT" ? "no such file" : /** @type {Error} */err.message;
|
|
85
265
|
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`, {
|
|
86
266
|
cause: err
|
|
87
267
|
});
|
|
@@ -96,34 +276,49 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
96
276
|
if (Object.keys(bundlesSources).length === 0) {
|
|
97
277
|
bundlesSources = null;
|
|
98
278
|
parsedModules = null;
|
|
99
|
-
logger.warn(
|
|
279
|
+
logger.warn("\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n");
|
|
100
280
|
}
|
|
101
281
|
}
|
|
282
|
+
|
|
283
|
+
/** @typedef {{ size: number, parsedSize?: number, gzipSize?: number, brotliSize?: number, zstdSize?: number, modules: StatsModule[], tree: Folder }} Asset */
|
|
284
|
+
|
|
102
285
|
const assets = bundleStats.assets.reduce((result, statAsset) => {
|
|
103
286
|
// If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
|
|
104
287
|
const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
|
|
105
|
-
|
|
106
|
-
const
|
|
288
|
+
/** @type {StatsModule[]} */
|
|
289
|
+
const modules = assetBundles ?
|
|
290
|
+
// @ts-expect-error TODO looks like we have a bug with child compilation parsing, need to add test cases
|
|
291
|
+
getBundleModules(assetBundles) : [];
|
|
292
|
+
const asset = result[statAsset.name] = /** @type {Asset} */{
|
|
107
293
|
size: statAsset.size
|
|
108
294
|
};
|
|
109
|
-
const assetSources = bundlesSources && Object.
|
|
295
|
+
const assetSources = bundlesSources && Object.hasOwn(bundlesSources, statAsset.name) ? bundlesSources[statAsset.name] : null;
|
|
110
296
|
if (assetSources) {
|
|
111
297
|
asset.parsedSize = Buffer.byteLength(assetSources.src);
|
|
112
|
-
if (compressionAlgorithm ===
|
|
113
|
-
|
|
298
|
+
if (compressionAlgorithm === "gzip") {
|
|
299
|
+
asset.gzipSize = getCompressedSize("gzip", assetSources.src);
|
|
300
|
+
}
|
|
301
|
+
if (compressionAlgorithm === "brotli") {
|
|
302
|
+
asset.brotliSize = getCompressedSize("brotli", assetSources.src);
|
|
303
|
+
}
|
|
304
|
+
if (compressionAlgorithm === "zstd") {
|
|
305
|
+
asset.zstdSize = getCompressedSize("zstd", assetSources.src);
|
|
306
|
+
}
|
|
114
307
|
}
|
|
115
308
|
|
|
116
309
|
// Picking modules from current bundle script
|
|
310
|
+
/** @type {StatsModule[]} */
|
|
117
311
|
let assetModules = (modules || []).filter(statModule => assetHasModule(statAsset, statModule));
|
|
118
312
|
|
|
119
313
|
// Adding parsed sources
|
|
120
314
|
if (parsedModules) {
|
|
315
|
+
/** @type {StatsModule[]} */
|
|
121
316
|
const unparsedEntryModules = [];
|
|
122
|
-
for (const
|
|
123
|
-
if (parsedModules[
|
|
124
|
-
|
|
125
|
-
} else if (isEntryModule(
|
|
126
|
-
unparsedEntryModules.push(
|
|
317
|
+
for (const statsModule of assetModules) {
|
|
318
|
+
if (typeof statsModule.id !== "undefined" && parsedModules[statsModule.id]) {
|
|
319
|
+
statsModule.parsedSrc = parsedModules[statsModule.id];
|
|
320
|
+
} else if (isEntryModule(statsModule)) {
|
|
321
|
+
unparsedEntryModules.push(statsModule);
|
|
127
322
|
}
|
|
128
323
|
}
|
|
129
324
|
|
|
@@ -139,10 +334,10 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
139
334
|
// If there are multiple entry points we move all of them under synthetic concatenated module.
|
|
140
335
|
assetModules = (assetModules || []).filter(mod => !unparsedEntryModules.includes(mod));
|
|
141
336
|
assetModules.unshift({
|
|
142
|
-
identifier:
|
|
143
|
-
name:
|
|
337
|
+
identifier: "./entry modules",
|
|
338
|
+
name: "./entry modules",
|
|
144
339
|
modules: unparsedEntryModules,
|
|
145
|
-
size: unparsedEntryModules.reduce((totalSize, module) => totalSize + module.size, 0),
|
|
340
|
+
size: unparsedEntryModules.reduce((totalSize, module) => totalSize + (/** @type {number} */module.size), 0),
|
|
146
341
|
parsedSrc: assetSources.runtimeSrc
|
|
147
342
|
});
|
|
148
343
|
}
|
|
@@ -153,7 +348,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
153
348
|
compressionAlgorithm
|
|
154
349
|
});
|
|
155
350
|
return result;
|
|
156
|
-
}, {});
|
|
351
|
+
}, /** @type {Record<string, Asset>} */{});
|
|
157
352
|
const chunkToInitialByEntrypoint = getChunkToInitialByEntrypoint(bundleStats);
|
|
158
353
|
return Object.entries(assets).map(([filename, asset]) => ({
|
|
159
354
|
label: filename,
|
|
@@ -166,88 +361,22 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
166
361
|
parsedSize: asset.parsedSize,
|
|
167
362
|
gzipSize: asset.gzipSize,
|
|
168
363
|
brotliSize: asset.brotliSize,
|
|
364
|
+
zstdSize: asset.zstdSize,
|
|
169
365
|
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
|
|
170
366
|
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
|
|
171
367
|
}));
|
|
172
368
|
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {string} filename filename
|
|
372
|
+
* @returns {Promise<StatsCompilation>} result
|
|
373
|
+
*/
|
|
173
374
|
function readStatsFromFile(filename) {
|
|
174
375
|
return parseChunked(fs.createReadStream(filename, {
|
|
175
|
-
encoding:
|
|
376
|
+
encoding: "utf8"
|
|
176
377
|
}));
|
|
177
378
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const seenIds = new Set();
|
|
183
|
-
return flatten((bundleStats.chunks?.map(chunk => chunk.modules) || []).concat(bundleStats.modules).filter(Boolean)).filter(mod => {
|
|
184
|
-
// Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
|
|
185
|
-
if (isRuntimeModule(mod)) {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
if (seenIds.has(mod.id)) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
seenIds.add(mod.id);
|
|
192
|
-
return true;
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
function assetHasModule(statAsset, statModule) {
|
|
196
|
-
// Checking if this module is the part of asset chunks
|
|
197
|
-
return (statModule.chunks || []).some(moduleChunk => statAsset.chunks.includes(moduleChunk));
|
|
198
|
-
}
|
|
199
|
-
function isEntryModule(statModule) {
|
|
200
|
-
return statModule.depth === 0;
|
|
201
|
-
}
|
|
202
|
-
function isRuntimeModule(statModule) {
|
|
203
|
-
return statModule.moduleType === 'runtime';
|
|
204
|
-
}
|
|
205
|
-
function createModulesTree(modules, opts) {
|
|
206
|
-
const root = new Folder('.', opts);
|
|
207
|
-
modules.forEach(module => root.addModule(module));
|
|
208
|
-
root.mergeNestedFolders();
|
|
209
|
-
return root;
|
|
210
|
-
}
|
|
211
|
-
function getChunkToInitialByEntrypoint(bundleStats) {
|
|
212
|
-
if (bundleStats == null) {
|
|
213
|
-
return {};
|
|
214
|
-
}
|
|
215
|
-
const chunkToEntrypointInititalMap = {};
|
|
216
|
-
Object.values(bundleStats.entrypoints || {}).forEach(entrypoint => {
|
|
217
|
-
for (const asset of entrypoint.assets) {
|
|
218
|
-
chunkToEntrypointInititalMap[asset.name] = chunkToEntrypointInititalMap[asset.name] ?? {};
|
|
219
|
-
chunkToEntrypointInititalMap[asset.name][entrypoint.name] = true;
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
return chunkToEntrypointInititalMap;
|
|
223
|
-
}
|
|
224
|
-
;
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* arr-flatten <https://github.com/jonschlinkert/arr-flatten>
|
|
228
|
-
*
|
|
229
|
-
* Copyright (c) 2014-2017, Jon Schlinkert.
|
|
230
|
-
* Released under the MIT License.
|
|
231
|
-
*
|
|
232
|
-
* Modified by Sukka <https://skk.moe>
|
|
233
|
-
*
|
|
234
|
-
* Replace recursively flatten with one-level deep flatten to match lodash.flatten
|
|
235
|
-
*
|
|
236
|
-
* TODO: replace with Array.prototype.flat once Node.js 10 support is dropped
|
|
237
|
-
*/
|
|
238
|
-
function flatten(arr) {
|
|
239
|
-
if (!arr) return [];
|
|
240
|
-
const len = arr.length;
|
|
241
|
-
if (!len) return [];
|
|
242
|
-
let cur;
|
|
243
|
-
const res = [];
|
|
244
|
-
for (let i = 0; i < len; i++) {
|
|
245
|
-
cur = arr[i];
|
|
246
|
-
if (Array.isArray(cur)) {
|
|
247
|
-
res.push(...cur);
|
|
248
|
-
} else {
|
|
249
|
-
res.push(cur);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return res;
|
|
253
|
-
}
|
|
379
|
+
module.exports = {
|
|
380
|
+
getViewerData,
|
|
381
|
+
readStatsFromFile
|
|
382
|
+
};
|
package/lib/bin/analyzer.js
CHANGED
|
@@ -2,30 +2,49 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} = require(
|
|
8
|
-
const
|
|
5
|
+
dirname,
|
|
6
|
+
resolve
|
|
7
|
+
} = require("node:path");
|
|
8
|
+
const {
|
|
9
|
+
program: commanderProgram
|
|
10
|
+
} = require("commander");
|
|
9
11
|
const {
|
|
10
12
|
magenta
|
|
11
|
-
} = require(
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
13
|
+
} = require("picocolors");
|
|
14
|
+
const Logger = require("../Logger");
|
|
15
|
+
const analyzer = require("../analyzer");
|
|
16
|
+
const {
|
|
17
|
+
isZstdSupported
|
|
18
|
+
} = require("../sizeUtils");
|
|
19
|
+
const utils = require("../utils");
|
|
20
|
+
const viewer = require("../viewer");
|
|
21
|
+
const SIZES = new Set(["stat", "parsed", "gzip"]);
|
|
22
|
+
const COMPRESSION_ALGORITHMS = new Set(isZstdSupported ? ["gzip", "brotli", "zstd"] : ["gzip", "brotli"]);
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} str string
|
|
26
|
+
* @returns {string} break with string
|
|
27
|
+
*/
|
|
28
|
+
function br(str) {
|
|
29
|
+
return `\n${" ".repeat(32)}${str}`;
|
|
30
|
+
}
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
/**
|
|
33
|
+
* @template T
|
|
34
|
+
* @returns {(val: T) => T[]} array
|
|
35
|
+
*/
|
|
36
|
+
function array() {
|
|
37
|
+
/** @type {T[]} */
|
|
38
|
+
const arr = [];
|
|
39
|
+
return val => {
|
|
40
|
+
arr.push(val);
|
|
41
|
+
return arr;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const program = commanderProgram.version(require("../../package.json").version).argument("<bundleStatsFile>", "Path to Webpack Stats JSON file.").argument("[bundleDir]", "Directory containing all generated bundles. You should provided it if you want analyzer to show you the real parsed module sizes. By default a directory of stats file is used.").option("-m, --mode <mode>", `Analyzer mode. Should be \`server\`,\`static\` or \`json\`.${br("In `server` mode analyzer will start HTTP server to show bundle report.")}${br("In `static` mode single HTML file with bundle report will be generated.")}${br("In `json` mode single JSON file with bundle report will be generated.")}`, "server").option(
|
|
26
45
|
// Had to make `host` parameter optional in order to let `-h` flag output help message
|
|
27
46
|
// Fixes https://github.com/webpack/webpack-bundle-analyzer/issues/239
|
|
28
|
-
|
|
47
|
+
"-h, --host [host]", "Host that will be used in `server` mode to start HTTP server.", "127.0.0.1").option("-p, --port <n>", "Port that will be used in `server` mode to start HTTP server.", "8888").option("-r, --report <file>", "Path to bundle report file that will be generated in `static` mode.").option("-t, --title <title>", "String to use in title element of html report.").option("-s, --default-sizes <type>", `Module sizes to show in treemap by default.${br(`Possible values: ${[...SIZES].join(", ")}`)}`, "parsed").option("--compression-algorithm <type>", `Compression algorithm that will be used to calculate the compressed module sizes.${br(`Possible values: ${[...COMPRESSION_ALGORITHMS].join(", ")}`)}`, "gzip").option("-O, --no-open", "Don't open report in default browser automatically.").option("-e, --exclude <regexp>", `Assets that should be excluded from the report.${br("Can be specified multiple times.")}`, array()).option("-l, --log-level <level>", `Log level.${br(`Possible values: ${[...Logger.levels].join(", ")}`)}`, Logger.defaultLevel).parse();
|
|
29
48
|
let [bundleStatsFile, bundleDir] = program.args;
|
|
30
49
|
let {
|
|
31
50
|
mode,
|
|
@@ -40,29 +59,48 @@ let {
|
|
|
40
59
|
exclude: excludeAssets
|
|
41
60
|
} = program.opts();
|
|
42
61
|
const logger = new Logger(logLevel);
|
|
43
|
-
if (typeof reportTitle ===
|
|
62
|
+
if (typeof reportTitle === "undefined") {
|
|
44
63
|
reportTitle = utils.defaultTitle;
|
|
45
64
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} error error message
|
|
68
|
+
*/
|
|
69
|
+
function showHelp(error) {
|
|
70
|
+
if (error) console.log(`\n ${magenta(error)}\n`);
|
|
71
|
+
program.outputHelp();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (!bundleStatsFile) {
|
|
75
|
+
showHelp("Provide path to Webpack Stats file as first argument");
|
|
49
76
|
}
|
|
50
|
-
if (mode
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
if (mode !== "server" && mode !== "static" && mode !== "json") {
|
|
78
|
+
showHelp("Invalid mode. Should be either `server`, `static` or `json`.");
|
|
79
|
+
}
|
|
80
|
+
if (mode === "server") {
|
|
81
|
+
if (!host) showHelp("Invalid host name");
|
|
82
|
+
port = port === "auto" ? 0 : Number(port);
|
|
83
|
+
if (Number.isNaN(port)) {
|
|
84
|
+
showHelp("Invalid port. Should be a number or `auto`");
|
|
85
|
+
}
|
|
54
86
|
}
|
|
55
87
|
if (!COMPRESSION_ALGORITHMS.has(compressionAlgorithm)) {
|
|
56
|
-
showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(
|
|
88
|
+
showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
if (!SIZES.has(defaultSizes)) {
|
|
91
|
+
showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(", ")}`);
|
|
57
92
|
}
|
|
58
|
-
if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
|
|
59
93
|
bundleStatsFile = resolve(bundleStatsFile);
|
|
60
94
|
if (!bundleDir) bundleDir = dirname(bundleStatsFile);
|
|
61
|
-
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} bundleStatsFile bundle stats file
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
62
100
|
async function parseAndAnalyse(bundleStatsFile) {
|
|
63
101
|
try {
|
|
64
102
|
const bundleStats = await analyzer.readStatsFromFile(bundleStatsFile);
|
|
65
|
-
if (mode ===
|
|
103
|
+
if (mode === "server") {
|
|
66
104
|
viewer.startServer(bundleStats, {
|
|
67
105
|
openBrowser,
|
|
68
106
|
port,
|
|
@@ -75,10 +113,10 @@ async function parseAndAnalyse(bundleStatsFile) {
|
|
|
75
113
|
logger: new Logger(logLevel),
|
|
76
114
|
analyzerUrl: utils.defaultAnalyzerUrl
|
|
77
115
|
});
|
|
78
|
-
} else if (mode ===
|
|
116
|
+
} else if (mode === "static") {
|
|
79
117
|
viewer.generateReport(bundleStats, {
|
|
80
118
|
openBrowser,
|
|
81
|
-
reportFilename: resolve(reportFilename ||
|
|
119
|
+
reportFilename: resolve(reportFilename || "report.html"),
|
|
82
120
|
reportTitle,
|
|
83
121
|
defaultSizes,
|
|
84
122
|
compressionAlgorithm,
|
|
@@ -86,9 +124,9 @@ async function parseAndAnalyse(bundleStatsFile) {
|
|
|
86
124
|
excludeAssets,
|
|
87
125
|
logger: new Logger(logLevel)
|
|
88
126
|
});
|
|
89
|
-
} else if (mode ===
|
|
127
|
+
} else if (mode === "json") {
|
|
90
128
|
viewer.generateJSONReport(bundleStats, {
|
|
91
|
-
reportFilename: resolve(reportFilename ||
|
|
129
|
+
reportFilename: resolve(reportFilename || "report.json"),
|
|
92
130
|
compressionAlgorithm,
|
|
93
131
|
bundleDir,
|
|
94
132
|
excludeAssets,
|
|
@@ -97,22 +135,8 @@ async function parseAndAnalyse(bundleStatsFile) {
|
|
|
97
135
|
}
|
|
98
136
|
} catch (err) {
|
|
99
137
|
logger.error(`Couldn't read webpack bundle stats from "${bundleStatsFile}":\n${err}`);
|
|
100
|
-
logger.debug(err.stack);
|
|
138
|
+
logger.debug(/** @type {Error} */err.stack);
|
|
101
139
|
process.exit(1);
|
|
102
140
|
}
|
|
103
141
|
}
|
|
104
|
-
|
|
105
|
-
if (error) console.log(`\n ${magenta(error)}\n`);
|
|
106
|
-
program.outputHelp();
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
function br(str) {
|
|
110
|
-
return `\n${' '.repeat(32)}${str}`;
|
|
111
|
-
}
|
|
112
|
-
function array() {
|
|
113
|
-
const arr = [];
|
|
114
|
-
return val => {
|
|
115
|
-
arr.push(val);
|
|
116
|
-
return arr;
|
|
117
|
-
};
|
|
118
|
-
}
|
|
142
|
+
parseAndAnalyse(bundleStatsFile);
|