webpack-bundle-analyzer 4.10.2 → 5.0.1
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 +30 -23
- package/lib/BundleAnalyzerPlugin.js +6 -27
- package/lib/Logger.js +2 -9
- package/lib/analyzer.js +53 -80
- package/lib/bin/analyzer.js +13 -21
- package/lib/index.js +0 -1
- package/lib/parseUtils.js +56 -74
- package/lib/sizeUtils.js +14 -0
- package/lib/statsUtils.js +0 -16
- package/lib/template.js +3 -10
- package/lib/tree/BaseFolder.js +3 -25
- package/lib/tree/ConcatenatedModule.js +11 -34
- package/lib/tree/ContentFolder.js +7 -12
- package/lib/tree/ContentModule.js +6 -12
- package/lib/tree/Folder.js +26 -28
- package/lib/tree/Module.js +20 -25
- package/lib/tree/Node.js +0 -7
- package/lib/tree/utils.js +8 -7
- package/lib/utils.js +2 -12
- package/lib/viewer.js +24 -34
- package/package.json +21 -22
- package/public/viewer.js +3 -3
- package/public/viewer.js.map +1 -1
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 or Brotli 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`|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 (default: parsed)
|
|
126
|
+
--compression-algorithm <type> Compression algorithm that will be used to calculate the compressed module sizes.
|
|
127
|
+
Possible values: gzip, brotli (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,10 @@ 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
|
+
|
|
154
161
|
<h2 align="center">Selecting Which Chunks to Display</h2>
|
|
155
162
|
|
|
156
163
|
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 +183,7 @@ It happens when `webpack-bundle-analyzer` analyzes files that don't actually exi
|
|
|
176
183
|
Error parsing bundle asset "your_bundle_name.bundle.js": no such file
|
|
177
184
|
No bundles were parsed. Analyzer will show only original module sizes from stats file.
|
|
178
185
|
```
|
|
179
|
-
To get more information about it you can read [issue #147](https://github.com/webpack
|
|
186
|
+
To get more information about it you can read [issue #147](https://github.com/webpack/webpack-bundle-analyzer/issues/147).
|
|
180
187
|
|
|
181
188
|
<h2 align="center">Other tools</h2>
|
|
182
189
|
|
|
@@ -210,8 +217,8 @@ To get more information about it you can read [issue #147](https://github.com/we
|
|
|
210
217
|
[node]: https://img.shields.io/node/v/webpack-bundle-analyzer.svg
|
|
211
218
|
[node-url]: https://nodejs.org
|
|
212
219
|
|
|
213
|
-
[tests]:
|
|
214
|
-
[tests-url]: https://
|
|
220
|
+
[tests]: https://github.com/webpack/webpack-bundle-analyzer/actions/workflows/main.yml/badge.svg
|
|
221
|
+
[tests-url]: https://github.com/webpack/webpack-bundle-analyzer/actions/workflows/main.yml
|
|
215
222
|
|
|
216
223
|
[downloads]: https://img.shields.io/npm/dt/webpack-bundle-analyzer.svg
|
|
217
224
|
[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
7
|
} = require('picocolors');
|
|
10
|
-
|
|
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,48 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
|
|
5
4
|
const path = require('path');
|
|
6
|
-
|
|
7
|
-
const gzipSize = require('gzip-size');
|
|
8
|
-
|
|
9
5
|
const {
|
|
10
6
|
parseChunked
|
|
11
7
|
} = require('@discoveryjs/json-ext');
|
|
12
|
-
|
|
13
8
|
const Logger = require('./Logger');
|
|
14
|
-
|
|
15
9
|
const Folder = require('./tree/Folder').default;
|
|
16
|
-
|
|
17
10
|
const {
|
|
18
11
|
parseBundle
|
|
19
12
|
} = require('./parseUtils');
|
|
20
|
-
|
|
21
13
|
const {
|
|
22
14
|
createAssetsFilter
|
|
23
15
|
} = require('./utils');
|
|
24
|
-
|
|
16
|
+
const {
|
|
17
|
+
getCompressedSize
|
|
18
|
+
} = require('./sizeUtils');
|
|
25
19
|
const FILENAME_QUERY_REGEXP = /\?.*$/u;
|
|
26
|
-
const FILENAME_EXTENSIONS = /\.(js|mjs|cjs)$/iu;
|
|
20
|
+
const FILENAME_EXTENSIONS = /\.(js|mjs|cjs|bundle)$/iu;
|
|
27
21
|
module.exports = {
|
|
28
22
|
getViewerData,
|
|
29
23
|
readStatsFromFile
|
|
30
24
|
};
|
|
31
|
-
|
|
32
25
|
function getViewerData(bundleStats, bundleDir, opts) {
|
|
33
26
|
const {
|
|
34
27
|
logger = new Logger(),
|
|
28
|
+
compressionAlgorithm,
|
|
35
29
|
excludeAssets = null
|
|
36
30
|
} = opts || {};
|
|
37
|
-
const isAssetIncluded = createAssetsFilter(excludeAssets);
|
|
31
|
+
const isAssetIncluded = createAssetsFilter(excludeAssets);
|
|
38
32
|
|
|
33
|
+
// Sometimes all the information is located in `children` array (e.g. problem in #10)
|
|
39
34
|
if ((bundleStats.assets == null || bundleStats.assets.length === 0) && bundleStats.children && bundleStats.children.length > 0) {
|
|
40
35
|
const {
|
|
41
36
|
children
|
|
42
37
|
} = bundleStats;
|
|
43
|
-
bundleStats = bundleStats.children[0];
|
|
38
|
+
bundleStats = bundleStats.children[0];
|
|
39
|
+
// Sometimes if there are additional child chunks produced add them as child assets,
|
|
44
40
|
// leave the 1st one as that is considered the 'root' asset.
|
|
45
|
-
|
|
46
41
|
for (let i = 1; i < children.length; i++) {
|
|
47
42
|
children[i].assets.forEach(asset => {
|
|
48
43
|
asset.isChild = true;
|
|
@@ -57,54 +52,53 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
57
52
|
bundleStats.assets.push(asset);
|
|
58
53
|
});
|
|
59
54
|
});
|
|
60
|
-
}
|
|
61
|
-
|
|
55
|
+
}
|
|
62
56
|
|
|
57
|
+
// Picking only `*.js, *.cjs or *.mjs` assets from bundle that has non-empty `chunks` array
|
|
63
58
|
bundleStats.assets = bundleStats.assets.filter(asset => {
|
|
64
59
|
// Filter out non 'asset' type asset if type is provided (Webpack 5 add a type to indicate asset types)
|
|
65
60
|
if (asset.type && asset.type !== 'asset') {
|
|
66
61
|
return false;
|
|
67
|
-
}
|
|
68
|
-
// See #22
|
|
69
|
-
|
|
62
|
+
}
|
|
70
63
|
|
|
64
|
+
// Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
|
|
65
|
+
// See #22
|
|
71
66
|
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
|
|
72
67
|
return FILENAME_EXTENSIONS.test(asset.name) && asset.chunks.length > 0 && isAssetIncluded(asset.name);
|
|
73
|
-
});
|
|
68
|
+
});
|
|
74
69
|
|
|
70
|
+
// Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
|
|
75
71
|
let bundlesSources = null;
|
|
76
72
|
let parsedModules = null;
|
|
77
|
-
|
|
78
73
|
if (bundleDir) {
|
|
79
74
|
bundlesSources = {};
|
|
80
75
|
parsedModules = {};
|
|
81
|
-
|
|
82
76
|
for (const statAsset of bundleStats.assets) {
|
|
83
77
|
const assetFile = path.join(bundleDir, statAsset.name);
|
|
84
78
|
let bundleInfo;
|
|
85
|
-
|
|
86
79
|
try {
|
|
87
|
-
bundleInfo = parseBundle(assetFile
|
|
80
|
+
bundleInfo = parseBundle(assetFile, {
|
|
81
|
+
sourceType: statAsset.info.javascriptModule ? 'module' : 'script'
|
|
82
|
+
});
|
|
88
83
|
} catch (err) {
|
|
89
84
|
const msg = err.code === 'ENOENT' ? 'no such file' : err.message;
|
|
90
|
-
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}
|
|
85
|
+
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`, {
|
|
86
|
+
cause: err
|
|
87
|
+
});
|
|
91
88
|
continue;
|
|
92
89
|
}
|
|
93
|
-
|
|
94
90
|
bundlesSources[statAsset.name] = {
|
|
95
91
|
src: bundleInfo.src,
|
|
96
92
|
runtimeSrc: bundleInfo.runtimeSrc
|
|
97
93
|
};
|
|
98
94
|
Object.assign(parsedModules, bundleInfo.modules);
|
|
99
95
|
}
|
|
100
|
-
|
|
101
96
|
if (Object.keys(bundlesSources).length === 0) {
|
|
102
97
|
bundlesSources = null;
|
|
103
98
|
parsedModules = null;
|
|
104
99
|
logger.warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n');
|
|
105
100
|
}
|
|
106
101
|
}
|
|
107
|
-
|
|
108
102
|
const assets = bundleStats.assets.reduce((result, statAsset) => {
|
|
109
103
|
// If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
|
|
110
104
|
const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
|
|
@@ -113,29 +107,29 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
113
107
|
size: statAsset.size
|
|
114
108
|
};
|
|
115
109
|
const assetSources = bundlesSources && Object.prototype.hasOwnProperty.call(bundlesSources, statAsset.name) ? bundlesSources[statAsset.name] : null;
|
|
116
|
-
|
|
117
110
|
if (assetSources) {
|
|
118
111
|
asset.parsedSize = Buffer.byteLength(assetSources.src);
|
|
119
|
-
asset.gzipSize =
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
if (compressionAlgorithm === 'gzip') asset.gzipSize = getCompressedSize('gzip', assetSources.src);
|
|
113
|
+
if (compressionAlgorithm === 'brotli') asset.brotliSize = getCompressedSize('brotli', assetSources.src);
|
|
114
|
+
}
|
|
122
115
|
|
|
123
|
-
|
|
116
|
+
// Picking modules from current bundle script
|
|
117
|
+
let assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule));
|
|
124
118
|
|
|
119
|
+
// Adding parsed sources
|
|
125
120
|
if (parsedModules) {
|
|
126
121
|
const unparsedEntryModules = [];
|
|
127
|
-
|
|
128
122
|
for (const statModule of assetModules) {
|
|
129
123
|
if (parsedModules[statModule.id]) {
|
|
130
124
|
statModule.parsedSrc = parsedModules[statModule.id];
|
|
131
125
|
} else if (isEntryModule(statModule)) {
|
|
132
126
|
unparsedEntryModules.push(statModule);
|
|
133
127
|
}
|
|
134
|
-
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
|
|
135
131
|
// Because of this they basically become a concatenated module, for which we can't even precisely determine its
|
|
136
132
|
// parsed source as it's located in the same scope as all Webpack runtime helpers.
|
|
137
|
-
|
|
138
|
-
|
|
139
133
|
if (unparsedEntryModules.length && assetSources) {
|
|
140
134
|
if (unparsedEntryModules.length === 1) {
|
|
141
135
|
// So if there is only one entry we consider its parsed source to be all the bundle code excluding code
|
|
@@ -154,98 +148,81 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
154
148
|
}
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
|
-
|
|
158
151
|
asset.modules = assetModules;
|
|
159
|
-
asset.tree = createModulesTree(asset.modules
|
|
152
|
+
asset.tree = createModulesTree(asset.modules, {
|
|
153
|
+
compressionAlgorithm
|
|
154
|
+
});
|
|
160
155
|
return result;
|
|
161
156
|
}, {});
|
|
162
157
|
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
|
-
isInitialByEntrypoint: (_chunkToInitialByEntr = chunkToInitialByEntrypoint[filename]) !== null && _chunkToInitialByEntr !== void 0 ? _chunkToInitialByEntr : {}
|
|
178
|
-
};
|
|
179
|
-
});
|
|
158
|
+
return Object.entries(assets).map(([filename, asset]) => ({
|
|
159
|
+
label: filename,
|
|
160
|
+
isAsset: true,
|
|
161
|
+
// Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
|
|
162
|
+
// In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
|
|
163
|
+
// be the size of minified bundle.
|
|
164
|
+
// Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
|
|
165
|
+
statSize: asset.tree.size || asset.size,
|
|
166
|
+
parsedSize: asset.parsedSize,
|
|
167
|
+
gzipSize: asset.gzipSize,
|
|
168
|
+
brotliSize: asset.brotliSize,
|
|
169
|
+
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
|
|
170
|
+
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
|
|
171
|
+
}));
|
|
180
172
|
}
|
|
181
|
-
|
|
182
173
|
function readStatsFromFile(filename) {
|
|
183
174
|
return parseChunked(fs.createReadStream(filename, {
|
|
184
175
|
encoding: 'utf8'
|
|
185
176
|
}));
|
|
186
177
|
}
|
|
187
|
-
|
|
188
178
|
function getChildAssetBundles(bundleStats, assetName) {
|
|
189
179
|
return flatten((bundleStats.children || []).find(c => Object.values(c.assetsByChunkName))).includes(assetName);
|
|
190
180
|
}
|
|
191
|
-
|
|
192
181
|
function getBundleModules(bundleStats) {
|
|
193
|
-
var _bundleStats$chunks;
|
|
194
|
-
|
|
195
182
|
const seenIds = new Set();
|
|
196
|
-
return flatten((
|
|
183
|
+
return flatten((bundleStats.chunks?.map(chunk => chunk.modules) || []).concat(bundleStats.modules).filter(Boolean)).filter(mod => {
|
|
197
184
|
// Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
|
|
198
185
|
if (isRuntimeModule(mod)) {
|
|
199
186
|
return false;
|
|
200
187
|
}
|
|
201
|
-
|
|
202
188
|
if (seenIds.has(mod.id)) {
|
|
203
189
|
return false;
|
|
204
190
|
}
|
|
205
|
-
|
|
206
191
|
seenIds.add(mod.id);
|
|
207
192
|
return true;
|
|
208
193
|
});
|
|
209
194
|
}
|
|
210
|
-
|
|
211
195
|
function assetHasModule(statAsset, statModule) {
|
|
212
196
|
// Checking if this module is the part of asset chunks
|
|
213
197
|
return (statModule.chunks || []).some(moduleChunk => statAsset.chunks.includes(moduleChunk));
|
|
214
198
|
}
|
|
215
|
-
|
|
216
199
|
function isEntryModule(statModule) {
|
|
217
200
|
return statModule.depth === 0;
|
|
218
201
|
}
|
|
219
|
-
|
|
220
202
|
function isRuntimeModule(statModule) {
|
|
221
203
|
return statModule.moduleType === 'runtime';
|
|
222
204
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const root = new Folder('.');
|
|
205
|
+
function createModulesTree(modules, opts) {
|
|
206
|
+
const root = new Folder('.', opts);
|
|
226
207
|
modules.forEach(module => root.addModule(module));
|
|
227
208
|
root.mergeNestedFolders();
|
|
228
209
|
return root;
|
|
229
210
|
}
|
|
230
|
-
|
|
231
211
|
function getChunkToInitialByEntrypoint(bundleStats) {
|
|
232
212
|
if (bundleStats == null) {
|
|
233
213
|
return {};
|
|
234
214
|
}
|
|
235
|
-
|
|
236
215
|
const chunkToEntrypointInititalMap = {};
|
|
237
216
|
Object.values(bundleStats.entrypoints || {}).forEach(entrypoint => {
|
|
238
217
|
for (const asset of entrypoint.assets) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
chunkToEntrypointInititalMap[asset.name] = (_chunkToEntrypointIni = chunkToEntrypointInititalMap[asset.name]) !== null && _chunkToEntrypointIni !== void 0 ? _chunkToEntrypointIni : {};
|
|
218
|
+
chunkToEntrypointInititalMap[asset.name] = chunkToEntrypointInititalMap[asset.name] ?? {};
|
|
242
219
|
chunkToEntrypointInititalMap[asset.name][entrypoint.name] = true;
|
|
243
220
|
}
|
|
244
221
|
});
|
|
245
222
|
return chunkToEntrypointInititalMap;
|
|
246
223
|
}
|
|
247
|
-
|
|
248
224
|
;
|
|
225
|
+
|
|
249
226
|
/**
|
|
250
227
|
* arr-flatten <https://github.com/jonschlinkert/arr-flatten>
|
|
251
228
|
*
|
|
@@ -258,23 +235,19 @@ function getChunkToInitialByEntrypoint(bundleStats) {
|
|
|
258
235
|
*
|
|
259
236
|
* TODO: replace with Array.prototype.flat once Node.js 10 support is dropped
|
|
260
237
|
*/
|
|
261
|
-
|
|
262
238
|
function flatten(arr) {
|
|
263
239
|
if (!arr) return [];
|
|
264
240
|
const len = arr.length;
|
|
265
241
|
if (!len) return [];
|
|
266
242
|
let cur;
|
|
267
243
|
const res = [];
|
|
268
|
-
|
|
269
244
|
for (let i = 0; i < len; i++) {
|
|
270
245
|
cur = arr[i];
|
|
271
|
-
|
|
272
246
|
if (Array.isArray(cur)) {
|
|
273
247
|
res.push(...cur);
|
|
274
248
|
} else {
|
|
275
249
|
res.push(cur);
|
|
276
250
|
}
|
|
277
251
|
}
|
|
278
|
-
|
|
279
252
|
return res;
|
|
280
253
|
}
|