webpack-bundle-analyzer 4.9.0 → 5.2.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 -23
- package/lib/BundleAnalyzerPlugin.js +7 -28
- package/lib/Logger.js +2 -9
- package/lib/analyzer.js +110 -89
- package/lib/bin/analyzer.js +17 -22
- package/lib/index.js +0 -1
- package/lib/parseUtils.js +61 -81
- package/lib/sizeUtils.js +19 -0
- package/lib/statsUtils.js +0 -16
- package/lib/template.js +9 -14
- package/lib/tree/BaseFolder.js +6 -30
- package/lib/tree/ConcatenatedModule.js +21 -39
- package/lib/tree/ContentFolder.js +11 -12
- package/lib/tree/ContentModule.js +9 -12
- package/lib/tree/Folder.js +31 -31
- package/lib/tree/Module.js +28 -27
- package/lib/tree/Node.js +0 -7
- package/lib/tree/utils.js +8 -12
- package/lib/utils.js +4 -16
- package/lib/viewer.js +27 -37
- package/package.json +31 -26
- package/public/viewer.js +3 -3
- package/public/viewer.js.map +1 -1
- package/CHANGELOG.md +0 -501
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ This module will help you:
|
|
|
45
45
|
4. Optimize it!
|
|
46
46
|
|
|
47
47
|
And the best thing is it supports minified bundles! It parses them to get real size of bundled modules.
|
|
48
|
-
And it also shows their gzipped sizes!
|
|
48
|
+
And it also shows their gzipped, Brotli, or Zstandard sizes!
|
|
49
49
|
|
|
50
50
|
<h2 align="center">Options (for plugin)</h2>
|
|
51
51
|
|
|
@@ -61,7 +61,8 @@ new BundleAnalyzerPlugin(options?: object)
|
|
|
61
61
|
|**`analyzerUrl`**|`{Function}` called with `{ listenHost: string, listenHost: string, boundAddress: server.address}`. [server.address comes from Node.js](https://nodejs.org/api/net.html#serveraddress)| Default: `http://${listenHost}:${boundAddress.port}`. The URL printed to console with server mode.|
|
|
62
62
|
|**`reportFilename`**|`{String}`|Default: `report.html`. Path to bundle report file that will be generated in `static` mode. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
|
|
63
63
|
|**`reportTitle`**|`{String\|function}`|Default: function that returns pretty printed current date and time. Content of the HTML `title` element; or a function of the form `() => string` that provides the content.|
|
|
64
|
-
|**`defaultSizes`**|One of: `stat`, `parsed`, `gzip`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.|
|
|
64
|
+
|**`defaultSizes`**|One of: `stat`, `parsed`, `gzip`, `brotli`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.|
|
|
65
|
+
|**`compressionAlgorithm`**|One of: `gzip`, `brotli`, `zstd`|Default: `gzip`. Compression type used to calculate the compressed module sizes.|
|
|
65
66
|
|**`openAnalyzer`**|`{Boolean}`|Default: `true`. Automatically open report in default browser.|
|
|
66
67
|
|**`generateStatsFile`**|`{Boolean}`|Default: `false`. If `true`, webpack stats JSON file will be generated in bundle output directory|
|
|
67
68
|
|**`statsFilename`**|`{String}`|Default: `stats.json`. Name of webpack stats JSON file that will be generated if `generateStatsFile` is `true`. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
|
|
@@ -80,7 +81,7 @@ command:
|
|
|
80
81
|
webpack --profile --json > stats.json
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
If you're on Windows and using PowerShell, you can generate the stats file with this command to [avoid BOM issues](https://github.com/webpack
|
|
84
|
+
If you're on Windows and using PowerShell, you can generate the stats file with this command to [avoid BOM issues](https://github.com/webpack/webpack-bundle-analyzer/issues/47):
|
|
84
85
|
|
|
85
86
|
```
|
|
86
87
|
webpack --profile --json | Out-file 'stats.json' -Encoding OEM
|
|
@@ -111,23 +112,25 @@ Directory containing all generated bundles.
|
|
|
111
112
|
### `options`
|
|
112
113
|
|
|
113
114
|
```
|
|
114
|
-
-V, --version
|
|
115
|
-
-m, --mode <mode>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-h, --host <host>
|
|
120
|
-
-p, --port <n>
|
|
121
|
-
-r, --report <file>
|
|
122
|
-
-t, --title <title>
|
|
123
|
-
-s, --default-sizes <type>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
-
|
|
115
|
+
-V, --version output the version number
|
|
116
|
+
-m, --mode <mode> Analyzer mode. Should be `server`, `static` or `json`.
|
|
117
|
+
In `server` mode analyzer will start HTTP server to show bundle report.
|
|
118
|
+
In `static` mode single HTML file with bundle report will be generated.
|
|
119
|
+
In `json` mode single JSON file with bundle report will be generated. (default: server)
|
|
120
|
+
-h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
|
|
121
|
+
-p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
|
|
122
|
+
-r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html)
|
|
123
|
+
-t, --title <title> String to use in title element of html report. (default: pretty printed current date)
|
|
124
|
+
-s, --default-sizes <type> Module sizes to show in treemap by default.
|
|
125
|
+
Possible values: stat, parsed, gzip, brotli, zstd (default: parsed)
|
|
126
|
+
--compression-algorithm <type> Compression algorithm that will be used to calculate the compressed module sizes.
|
|
127
|
+
Possible values: gzip, brotli, zstd (default: gzip)
|
|
128
|
+
-O, --no-open Don't open report in default browser automatically.
|
|
129
|
+
-e, --exclude <regexp> Assets that should be excluded from the report.
|
|
130
|
+
Can be specified multiple times.
|
|
131
|
+
-l, --log-level <level> Log level.
|
|
132
|
+
Possible values: debug, info, warn, error, silent (default: info)
|
|
133
|
+
-h, --help output usage information
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
<h2 align="center" id="size-definitions">Size definitions</h2>
|
|
@@ -151,6 +154,14 @@ as Uglify, then this value will reflect the minified size of your code.
|
|
|
151
154
|
|
|
152
155
|
This is the size of running the parsed bundles/modules through gzip compression.
|
|
153
156
|
|
|
157
|
+
### `brotli`
|
|
158
|
+
|
|
159
|
+
This is the size of running the parsed bundles/modules through Brotli compression.
|
|
160
|
+
|
|
161
|
+
### `zstd`
|
|
162
|
+
|
|
163
|
+
This is the size of running the parsed bundles/modules through Zstandard compression. (Node.js 22.15.0+ is required for this feature)
|
|
164
|
+
|
|
154
165
|
<h2 align="center">Selecting Which Chunks to Display</h2>
|
|
155
166
|
|
|
156
167
|
When opened, the report displays all of the Webpack chunks for your project. It's possible to filter to a more specific list of chunks by using the sidebar or the chunk context menu.
|
|
@@ -176,7 +187,7 @@ It happens when `webpack-bundle-analyzer` analyzes files that don't actually exi
|
|
|
176
187
|
Error parsing bundle asset "your_bundle_name.bundle.js": no such file
|
|
177
188
|
No bundles were parsed. Analyzer will show only original module sizes from stats file.
|
|
178
189
|
```
|
|
179
|
-
To get more information about it you can read [issue #147](https://github.com/webpack
|
|
190
|
+
To get more information about it you can read [issue #147](https://github.com/webpack/webpack-bundle-analyzer/issues/147).
|
|
180
191
|
|
|
181
192
|
<h2 align="center">Other tools</h2>
|
|
182
193
|
|
|
@@ -210,8 +221,8 @@ To get more information about it you can read [issue #147](https://github.com/we
|
|
|
210
221
|
[node]: https://img.shields.io/node/v/webpack-bundle-analyzer.svg
|
|
211
222
|
[node-url]: https://nodejs.org
|
|
212
223
|
|
|
213
|
-
[tests]:
|
|
214
|
-
[tests-url]: https://
|
|
224
|
+
[tests]: https://github.com/webpack/webpack-bundle-analyzer/actions/workflows/main.yml/badge.svg
|
|
225
|
+
[tests-url]: https://github.com/webpack/webpack-bundle-analyzer/actions/workflows/main.yml
|
|
215
226
|
|
|
216
227
|
[downloads]: https://img.shields.io/npm/dt/webpack-bundle-analyzer.svg
|
|
217
228
|
[downloads-url]: https://npmjs.com/package/webpack-bundle-analyzer
|
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
|
|
5
4
|
const path = require('path');
|
|
6
|
-
|
|
7
5
|
const {
|
|
8
6
|
bold
|
|
9
|
-
} = require('
|
|
10
|
-
|
|
7
|
+
} = require('picocolors');
|
|
11
8
|
const Logger = require('./Logger');
|
|
12
|
-
|
|
13
9
|
const viewer = require('./viewer');
|
|
14
|
-
|
|
15
10
|
const utils = require('./utils');
|
|
16
|
-
|
|
17
11
|
const {
|
|
18
12
|
writeStats
|
|
19
13
|
} = require('./statsUtils');
|
|
20
|
-
|
|
21
14
|
class BundleAnalyzerPlugin {
|
|
22
15
|
constructor(opts = {}) {
|
|
23
16
|
this.opts = {
|
|
24
17
|
analyzerMode: 'server',
|
|
25
18
|
analyzerHost: '127.0.0.1',
|
|
19
|
+
compressionAlgorithm: 'gzip',
|
|
26
20
|
reportFilename: null,
|
|
27
21
|
reportTitle: utils.defaultTitle,
|
|
28
22
|
defaultSizes: 'parsed',
|
|
@@ -41,24 +35,19 @@ class BundleAnalyzerPlugin {
|
|
|
41
35
|
this.server = null;
|
|
42
36
|
this.logger = new Logger(this.opts.logLevel);
|
|
43
37
|
}
|
|
44
|
-
|
|
45
38
|
apply(compiler) {
|
|
46
39
|
this.compiler = compiler;
|
|
47
|
-
|
|
48
40
|
const done = (stats, callback) => {
|
|
49
41
|
callback = callback || (() => {});
|
|
50
|
-
|
|
51
42
|
const actions = [];
|
|
52
|
-
|
|
53
43
|
if (this.opts.generateStatsFile) {
|
|
54
44
|
actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions)));
|
|
55
|
-
}
|
|
56
|
-
|
|
45
|
+
}
|
|
57
46
|
|
|
47
|
+
// Handling deprecated `startAnalyzer` flag
|
|
58
48
|
if (this.opts.analyzerMode === 'server' && !this.opts.startAnalyzer) {
|
|
59
49
|
this.opts.analyzerMode = 'disabled';
|
|
60
50
|
}
|
|
61
|
-
|
|
62
51
|
if (this.opts.analyzerMode === 'server') {
|
|
63
52
|
actions.push(() => this.startAnalyzerServer(stats.toJson()));
|
|
64
53
|
} else if (this.opts.analyzerMode === 'static') {
|
|
@@ -66,7 +55,6 @@ class BundleAnalyzerPlugin {
|
|
|
66
55
|
} else if (this.opts.analyzerMode === 'json') {
|
|
67
56
|
actions.push(() => this.generateJSONReport(stats.toJson()));
|
|
68
57
|
}
|
|
69
|
-
|
|
70
58
|
if (actions.length) {
|
|
71
59
|
// Making analyzer logs to be after all webpack logs in the console
|
|
72
60
|
setImmediate(async () => {
|
|
@@ -81,20 +69,17 @@ class BundleAnalyzerPlugin {
|
|
|
81
69
|
callback();
|
|
82
70
|
}
|
|
83
71
|
};
|
|
84
|
-
|
|
85
72
|
if (compiler.hooks) {
|
|
86
73
|
compiler.hooks.done.tapAsync('webpack-bundle-analyzer', done);
|
|
87
74
|
} else {
|
|
88
75
|
compiler.plugin('done', done);
|
|
89
76
|
}
|
|
90
77
|
}
|
|
91
|
-
|
|
92
78
|
async generateStatsFile(stats) {
|
|
93
79
|
const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename);
|
|
94
80
|
await fs.promises.mkdir(path.dirname(statsFilepath), {
|
|
95
81
|
recursive: true
|
|
96
82
|
});
|
|
97
|
-
|
|
98
83
|
try {
|
|
99
84
|
await writeStats(stats, statsFilepath);
|
|
100
85
|
this.logger.info(`${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`);
|
|
@@ -102,7 +87,6 @@ class BundleAnalyzerPlugin {
|
|
|
102
87
|
this.logger.error(`${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}`);
|
|
103
88
|
}
|
|
104
89
|
}
|
|
105
|
-
|
|
106
90
|
async startAnalyzerServer(stats) {
|
|
107
91
|
if (this.server) {
|
|
108
92
|
(await this.server).updateChartData(stats);
|
|
@@ -112,6 +96,7 @@ class BundleAnalyzerPlugin {
|
|
|
112
96
|
host: this.opts.analyzerHost,
|
|
113
97
|
port: this.opts.analyzerPort,
|
|
114
98
|
reportTitle: this.opts.reportTitle,
|
|
99
|
+
compressionAlgorithm: this.opts.compressionAlgorithm,
|
|
115
100
|
bundleDir: this.getBundleDirFromCompiler(),
|
|
116
101
|
logger: this.logger,
|
|
117
102
|
defaultSizes: this.opts.defaultSizes,
|
|
@@ -120,47 +105,41 @@ class BundleAnalyzerPlugin {
|
|
|
120
105
|
});
|
|
121
106
|
}
|
|
122
107
|
}
|
|
123
|
-
|
|
124
108
|
async generateJSONReport(stats) {
|
|
125
109
|
await viewer.generateJSONReport(stats, {
|
|
126
110
|
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'),
|
|
111
|
+
compressionAlgorithm: this.opts.compressionAlgorithm,
|
|
127
112
|
bundleDir: this.getBundleDirFromCompiler(),
|
|
128
113
|
logger: this.logger,
|
|
129
114
|
excludeAssets: this.opts.excludeAssets
|
|
130
115
|
});
|
|
131
116
|
}
|
|
132
|
-
|
|
133
117
|
async generateStaticReport(stats) {
|
|
134
118
|
await viewer.generateReport(stats, {
|
|
135
119
|
openBrowser: this.opts.openAnalyzer,
|
|
136
120
|
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'),
|
|
137
121
|
reportTitle: this.opts.reportTitle,
|
|
122
|
+
compressionAlgorithm: this.opts.compressionAlgorithm,
|
|
138
123
|
bundleDir: this.getBundleDirFromCompiler(),
|
|
139
124
|
logger: this.logger,
|
|
140
125
|
defaultSizes: this.opts.defaultSizes,
|
|
141
126
|
excludeAssets: this.opts.excludeAssets
|
|
142
127
|
});
|
|
143
128
|
}
|
|
144
|
-
|
|
145
129
|
getBundleDirFromCompiler() {
|
|
146
130
|
if (typeof this.compiler.outputFileSystem.constructor === 'undefined') {
|
|
147
131
|
return this.compiler.outputPath;
|
|
148
132
|
}
|
|
149
|
-
|
|
150
133
|
switch (this.compiler.outputFileSystem.constructor.name) {
|
|
151
134
|
case 'MemoryFileSystem':
|
|
152
135
|
return null;
|
|
153
136
|
// Detect AsyncMFS used by Nuxt 2.5 that replaces webpack's MFS during development
|
|
154
137
|
// Related: #274
|
|
155
|
-
|
|
156
138
|
case 'AsyncMFS':
|
|
157
139
|
return null;
|
|
158
|
-
|
|
159
140
|
default:
|
|
160
141
|
return this.compiler.outputPath;
|
|
161
142
|
}
|
|
162
143
|
}
|
|
163
|
-
|
|
164
144
|
}
|
|
165
|
-
|
|
166
145
|
module.exports = BundleAnalyzerPlugin;
|
package/lib/Logger.js
CHANGED
|
@@ -2,35 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const LEVELS = ['debug', 'info', 'warn', 'error', 'silent'];
|
|
4
4
|
const LEVEL_TO_CONSOLE_METHOD = new Map([['debug', 'log'], ['info', 'log'], ['warn', 'log']]);
|
|
5
|
-
|
|
6
5
|
class Logger {
|
|
6
|
+
static levels = LEVELS;
|
|
7
|
+
static defaultLevel = 'info';
|
|
7
8
|
constructor(level = Logger.defaultLevel) {
|
|
8
9
|
this.activeLevels = new Set();
|
|
9
10
|
this.setLogLevel(level);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
12
|
setLogLevel(level) {
|
|
13
13
|
const levelIndex = LEVELS.indexOf(level);
|
|
14
14
|
if (levelIndex === -1) throw new Error(`Invalid log level "${level}". Use one of these: ${LEVELS.join(', ')}`);
|
|
15
15
|
this.activeLevels.clear();
|
|
16
|
-
|
|
17
16
|
for (const [i, level] of LEVELS.entries()) {
|
|
18
17
|
if (i >= levelIndex) this.activeLevels.add(level);
|
|
19
18
|
}
|
|
20
19
|
}
|
|
21
|
-
|
|
22
20
|
_log(level, ...args) {
|
|
23
21
|
console[LEVEL_TO_CONSOLE_METHOD.get(level) || level](...args);
|
|
24
22
|
}
|
|
25
|
-
|
|
26
23
|
}
|
|
27
|
-
|
|
28
|
-
Logger.levels = LEVELS;
|
|
29
|
-
Logger.defaultLevel = 'info';
|
|
30
24
|
;
|
|
31
25
|
LEVELS.forEach(level => {
|
|
32
26
|
if (level === 'silent') return;
|
|
33
|
-
|
|
34
27
|
Logger.prototype[level] = function (...args) {
|
|
35
28
|
if (this.activeLevels.has(level)) this._log(level, ...args);
|
|
36
29
|
};
|
package/lib/analyzer.js
CHANGED
|
@@ -1,57 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
|
|
5
4
|
const path = require('path');
|
|
6
|
-
|
|
7
|
-
const _ = require('lodash');
|
|
8
|
-
|
|
9
|
-
const gzipSize = require('gzip-size');
|
|
10
|
-
|
|
11
5
|
const {
|
|
12
6
|
parseChunked
|
|
13
7
|
} = require('@discoveryjs/json-ext');
|
|
14
|
-
|
|
15
8
|
const Logger = require('./Logger');
|
|
16
|
-
|
|
17
9
|
const Folder = require('./tree/Folder').default;
|
|
18
|
-
|
|
19
10
|
const {
|
|
20
11
|
parseBundle
|
|
21
12
|
} = require('./parseUtils');
|
|
22
|
-
|
|
23
13
|
const {
|
|
24
14
|
createAssetsFilter
|
|
25
15
|
} = require('./utils');
|
|
26
|
-
|
|
16
|
+
const {
|
|
17
|
+
getCompressedSize
|
|
18
|
+
} = require('./sizeUtils');
|
|
27
19
|
const FILENAME_QUERY_REGEXP = /\?.*$/u;
|
|
28
|
-
const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu;
|
|
20
|
+
const FILENAME_EXTENSIONS = /\.(js|mjs|cjs|bundle)$/iu;
|
|
29
21
|
module.exports = {
|
|
30
22
|
getViewerData,
|
|
31
23
|
readStatsFromFile
|
|
32
24
|
};
|
|
33
|
-
|
|
34
25
|
function getViewerData(bundleStats, bundleDir, opts) {
|
|
35
26
|
const {
|
|
36
27
|
logger = new Logger(),
|
|
28
|
+
compressionAlgorithm,
|
|
37
29
|
excludeAssets = null
|
|
38
30
|
} = opts || {};
|
|
39
|
-
const isAssetIncluded = createAssetsFilter(excludeAssets);
|
|
31
|
+
const isAssetIncluded = createAssetsFilter(excludeAssets);
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
// Sometimes all the information is located in `children` array (e.g. problem in #10)
|
|
34
|
+
if ((bundleStats.assets == null || bundleStats.assets.length === 0) && bundleStats.children && bundleStats.children.length > 0) {
|
|
42
35
|
const {
|
|
43
36
|
children
|
|
44
37
|
} = bundleStats;
|
|
45
|
-
bundleStats = bundleStats.children[0];
|
|
38
|
+
bundleStats = bundleStats.children[0];
|
|
39
|
+
// Sometimes if there are additional child chunks produced add them as child assets,
|
|
46
40
|
// leave the 1st one as that is considered the 'root' asset.
|
|
47
|
-
|
|
48
41
|
for (let i = 1; i < children.length; i++) {
|
|
49
42
|
children[i].assets.forEach(asset => {
|
|
50
43
|
asset.isChild = true;
|
|
51
44
|
bundleStats.assets.push(asset);
|
|
52
45
|
});
|
|
53
46
|
}
|
|
54
|
-
} else if (
|
|
47
|
+
} else if (bundleStats.children && bundleStats.children.length > 0) {
|
|
55
48
|
// Sometimes if there are additional child chunks produced add them as child assets
|
|
56
49
|
bundleStats.children.forEach(child => {
|
|
57
50
|
child.assets.forEach(asset => {
|
|
@@ -59,82 +52,85 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
59
52
|
bundleStats.assets.push(asset);
|
|
60
53
|
});
|
|
61
54
|
});
|
|
62
|
-
}
|
|
63
|
-
|
|
55
|
+
}
|
|
64
56
|
|
|
65
|
-
|
|
57
|
+
// Picking only `*.js, *.cjs or *.mjs` assets from bundle that has non-empty `chunks` array
|
|
58
|
+
bundleStats.assets = (bundleStats.assets || []).filter(asset => {
|
|
66
59
|
// Filter out non 'asset' type asset if type is provided (Webpack 5 add a type to indicate asset types)
|
|
67
60
|
if (asset.type && asset.type !== 'asset') {
|
|
68
61
|
return false;
|
|
69
|
-
}
|
|
70
|
-
// See #22
|
|
71
|
-
|
|
62
|
+
}
|
|
72
63
|
|
|
64
|
+
// Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
|
|
65
|
+
// See #22
|
|
73
66
|
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
|
|
74
|
-
return FILENAME_EXTENSIONS.test(asset.name) &&
|
|
75
|
-
});
|
|
67
|
+
return FILENAME_EXTENSIONS.test(asset.name) && asset.chunks.length > 0 && isAssetIncluded(asset.name);
|
|
68
|
+
});
|
|
76
69
|
|
|
70
|
+
// Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
|
|
77
71
|
let bundlesSources = null;
|
|
78
72
|
let parsedModules = null;
|
|
79
|
-
|
|
80
73
|
if (bundleDir) {
|
|
81
74
|
bundlesSources = {};
|
|
82
75
|
parsedModules = {};
|
|
83
|
-
|
|
84
76
|
for (const statAsset of bundleStats.assets) {
|
|
85
77
|
const assetFile = path.join(bundleDir, statAsset.name);
|
|
86
78
|
let bundleInfo;
|
|
87
|
-
|
|
88
79
|
try {
|
|
89
|
-
bundleInfo = parseBundle(assetFile
|
|
80
|
+
bundleInfo = parseBundle(assetFile, {
|
|
81
|
+
sourceType: statAsset.info.javascriptModule ? 'module' : 'script'
|
|
82
|
+
});
|
|
90
83
|
} catch (err) {
|
|
91
84
|
const msg = err.code === 'ENOENT' ? 'no such file' : err.message;
|
|
92
|
-
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}
|
|
85
|
+
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`, {
|
|
86
|
+
cause: err
|
|
87
|
+
});
|
|
93
88
|
continue;
|
|
94
89
|
}
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
bundlesSources[statAsset.name] = {
|
|
91
|
+
src: bundleInfo.src,
|
|
92
|
+
runtimeSrc: bundleInfo.runtimeSrc
|
|
93
|
+
};
|
|
97
94
|
Object.assign(parsedModules, bundleInfo.modules);
|
|
98
95
|
}
|
|
99
|
-
|
|
100
|
-
if (_.isEmpty(bundlesSources)) {
|
|
96
|
+
if (Object.keys(bundlesSources).length === 0) {
|
|
101
97
|
bundlesSources = null;
|
|
102
98
|
parsedModules = null;
|
|
103
99
|
logger.warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n');
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
|
-
|
|
107
102
|
const assets = bundleStats.assets.reduce((result, statAsset) => {
|
|
108
103
|
// If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
|
|
109
104
|
const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
|
|
110
105
|
const modules = assetBundles ? getBundleModules(assetBundles) : [];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const assetSources = bundlesSources &&
|
|
115
|
-
|
|
106
|
+
const asset = result[statAsset.name] = {
|
|
107
|
+
size: statAsset.size
|
|
108
|
+
};
|
|
109
|
+
const assetSources = bundlesSources && Object.prototype.hasOwnProperty.call(bundlesSources, statAsset.name) ? bundlesSources[statAsset.name] : null;
|
|
116
110
|
if (assetSources) {
|
|
117
111
|
asset.parsedSize = Buffer.byteLength(assetSources.src);
|
|
118
|
-
asset.gzipSize =
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
if (compressionAlgorithm === 'gzip') asset.gzipSize = getCompressedSize('gzip', assetSources.src);
|
|
113
|
+
if (compressionAlgorithm === 'brotli') asset.brotliSize = getCompressedSize('brotli', assetSources.src);
|
|
114
|
+
if (compressionAlgorithm === 'zstd') asset.zstdSize = getCompressedSize('zstd', assetSources.src);
|
|
115
|
+
}
|
|
121
116
|
|
|
122
|
-
|
|
117
|
+
// Picking modules from current bundle script
|
|
118
|
+
let assetModules = (modules || []).filter(statModule => assetHasModule(statAsset, statModule));
|
|
123
119
|
|
|
120
|
+
// Adding parsed sources
|
|
124
121
|
if (parsedModules) {
|
|
125
122
|
const unparsedEntryModules = [];
|
|
126
|
-
|
|
127
123
|
for (const statModule of assetModules) {
|
|
128
124
|
if (parsedModules[statModule.id]) {
|
|
129
125
|
statModule.parsedSrc = parsedModules[statModule.id];
|
|
130
126
|
} else if (isEntryModule(statModule)) {
|
|
131
127
|
unparsedEntryModules.push(statModule);
|
|
132
128
|
}
|
|
133
|
-
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
|
|
134
132
|
// Because of this they basically become a concatenated module, for which we can't even precisely determine its
|
|
135
133
|
// parsed source as it's located in the same scope as all Webpack runtime helpers.
|
|
136
|
-
|
|
137
|
-
|
|
138
134
|
if (unparsedEntryModules.length && assetSources) {
|
|
139
135
|
if (unparsedEntryModules.length === 1) {
|
|
140
136
|
// So if there is only one entry we consider its parsed source to be all the bundle code excluding code
|
|
@@ -142,8 +138,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
142
138
|
unparsedEntryModules[0].parsedSrc = assetSources.runtimeSrc;
|
|
143
139
|
} else {
|
|
144
140
|
// If there are multiple entry points we move all of them under synthetic concatenated module.
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
assetModules = (assetModules || []).filter(mod => !unparsedEntryModules.includes(mod));
|
|
147
142
|
assetModules.unshift({
|
|
148
143
|
identifier: './entry modules',
|
|
149
144
|
name: './entry modules',
|
|
@@ -154,81 +149,107 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
}
|
|
157
|
-
|
|
158
152
|
asset.modules = assetModules;
|
|
159
|
-
asset.tree = createModulesTree(asset.modules
|
|
153
|
+
asset.tree = createModulesTree(asset.modules, {
|
|
154
|
+
compressionAlgorithm
|
|
155
|
+
});
|
|
160
156
|
return result;
|
|
161
157
|
}, {});
|
|
162
158
|
const chunkToInitialByEntrypoint = getChunkToInitialByEntrypoint(bundleStats);
|
|
163
|
-
return Object.entries(assets).map(([filename, asset]) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
};
|
|
179
|
-
});
|
|
159
|
+
return Object.entries(assets).map(([filename, asset]) => ({
|
|
160
|
+
label: filename,
|
|
161
|
+
isAsset: true,
|
|
162
|
+
// Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
|
|
163
|
+
// In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
|
|
164
|
+
// be the size of minified bundle.
|
|
165
|
+
// Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
|
|
166
|
+
statSize: asset.tree.size || asset.size,
|
|
167
|
+
parsedSize: asset.parsedSize,
|
|
168
|
+
gzipSize: asset.gzipSize,
|
|
169
|
+
brotliSize: asset.brotliSize,
|
|
170
|
+
zstdSize: asset.zstdSize,
|
|
171
|
+
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
|
|
172
|
+
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
|
|
173
|
+
}));
|
|
180
174
|
}
|
|
181
|
-
|
|
182
175
|
function readStatsFromFile(filename) {
|
|
183
176
|
return parseChunked(fs.createReadStream(filename, {
|
|
184
177
|
encoding: 'utf8'
|
|
185
178
|
}));
|
|
186
179
|
}
|
|
187
|
-
|
|
188
180
|
function getChildAssetBundles(bundleStats, assetName) {
|
|
189
|
-
return (bundleStats.children || []).find(c =>
|
|
181
|
+
return flatten((bundleStats.children || []).find(c => Object.values(c.assetsByChunkName))).includes(assetName);
|
|
190
182
|
}
|
|
191
|
-
|
|
192
183
|
function getBundleModules(bundleStats) {
|
|
193
|
-
|
|
194
|
-
.
|
|
184
|
+
const seenIds = new Set();
|
|
185
|
+
return flatten((bundleStats.chunks?.map(chunk => chunk.modules) || []).concat(bundleStats.modules).filter(Boolean)).filter(mod => {
|
|
186
|
+
// Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
|
|
187
|
+
if (isRuntimeModule(mod)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (seenIds.has(mod.id)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
seenIds.add(mod.id);
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
195
196
|
}
|
|
196
|
-
|
|
197
197
|
function assetHasModule(statAsset, statModule) {
|
|
198
198
|
// Checking if this module is the part of asset chunks
|
|
199
199
|
return (statModule.chunks || []).some(moduleChunk => statAsset.chunks.includes(moduleChunk));
|
|
200
200
|
}
|
|
201
|
-
|
|
202
201
|
function isEntryModule(statModule) {
|
|
203
202
|
return statModule.depth === 0;
|
|
204
203
|
}
|
|
205
|
-
|
|
206
204
|
function isRuntimeModule(statModule) {
|
|
207
205
|
return statModule.moduleType === 'runtime';
|
|
208
206
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const root = new Folder('.');
|
|
207
|
+
function createModulesTree(modules, opts) {
|
|
208
|
+
const root = new Folder('.', opts);
|
|
212
209
|
modules.forEach(module => root.addModule(module));
|
|
213
210
|
root.mergeNestedFolders();
|
|
214
211
|
return root;
|
|
215
212
|
}
|
|
216
|
-
|
|
217
213
|
function getChunkToInitialByEntrypoint(bundleStats) {
|
|
218
214
|
if (bundleStats == null) {
|
|
219
215
|
return {};
|
|
220
216
|
}
|
|
221
|
-
|
|
222
217
|
const chunkToEntrypointInititalMap = {};
|
|
223
218
|
Object.values(bundleStats.entrypoints || {}).forEach(entrypoint => {
|
|
224
219
|
for (const asset of entrypoint.assets) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
chunkToEntrypointInititalMap[asset.name] = (_chunkToEntrypointIni = chunkToEntrypointInititalMap[asset.name]) !== null && _chunkToEntrypointIni !== void 0 ? _chunkToEntrypointIni : {};
|
|
220
|
+
chunkToEntrypointInititalMap[asset.name] = chunkToEntrypointInititalMap[asset.name] ?? {};
|
|
228
221
|
chunkToEntrypointInititalMap[asset.name][entrypoint.name] = true;
|
|
229
222
|
}
|
|
230
223
|
});
|
|
231
224
|
return chunkToEntrypointInititalMap;
|
|
232
225
|
}
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* arr-flatten <https://github.com/jonschlinkert/arr-flatten>
|
|
230
|
+
*
|
|
231
|
+
* Copyright (c) 2014-2017, Jon Schlinkert.
|
|
232
|
+
* Released under the MIT License.
|
|
233
|
+
*
|
|
234
|
+
* Modified by Sukka <https://skk.moe>
|
|
235
|
+
*
|
|
236
|
+
* Replace recursively flatten with one-level deep flatten to match lodash.flatten
|
|
237
|
+
*
|
|
238
|
+
* TODO: replace with Array.prototype.flat once Node.js 10 support is dropped
|
|
239
|
+
*/
|
|
240
|
+
function flatten(arr) {
|
|
241
|
+
if (!arr) return [];
|
|
242
|
+
const len = arr.length;
|
|
243
|
+
if (!len) return [];
|
|
244
|
+
let cur;
|
|
245
|
+
const res = [];
|
|
246
|
+
for (let i = 0; i < len; i++) {
|
|
247
|
+
cur = arr[i];
|
|
248
|
+
if (Array.isArray(cur)) {
|
|
249
|
+
res.push(...cur);
|
|
250
|
+
} else {
|
|
251
|
+
res.push(cur);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return res;
|
|
255
|
+
}
|