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 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-contrib/webpack-bundle-analyzer/issues/47):
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 output the version number
115
- -m, --mode <mode> Analyzer mode. Should be `server`, `static` or `json`.
116
- In `server` mode analyzer will start HTTP server to show bundle report.
117
- In `static` mode single HTML file with bundle report will be generated.
118
- In `json` mode single JSON file with bundle report will be generated. (default: server)
119
- -h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
120
- -p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
121
- -r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html)
122
- -t, --title <title> String to use in title element of html report. (default: pretty printed current date)
123
- -s, --default-sizes <type> Module sizes to show in treemap by default.
124
- Possible values: stat, parsed, gzip (default: parsed)
125
- -O, --no-open Don't open report in default browser automatically.
126
- -e, --exclude <regexp> Assets that should be excluded from the report.
127
- Can be specified multiple times.
128
- -l, --log-level <level> Log level.
129
- Possible values: debug, info, warn, error, silent (default: info)
130
- -h, --help output usage information
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-contrib/webpack-bundle-analyzer/issues/147).
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]: http://img.shields.io/travis/webpack-contrib/webpack-bundle-analyzer.svg
214
- [tests-url]: https://travis-ci.org/webpack-contrib/webpack-bundle-analyzer
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('chalk');
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
- } // Handling deprecated `startAnalyzer` flag
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); // Sometimes all the information is located in `children` array (e.g. problem in #10)
31
+ const isAssetIncluded = createAssetsFilter(excludeAssets);
40
32
 
41
- if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
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]; // Sometimes if there are additional child chunks produced add them as child assets,
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 (!_.isEmpty(bundleStats.children)) {
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
- } // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
63
-
55
+ }
64
56
 
65
- bundleStats.assets = bundleStats.assets.filter(asset => {
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
- } // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
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) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
75
- }); // Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
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
- bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
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
- const asset = result[statAsset.name] = _.pick(statAsset, 'size');
113
-
114
- const assetSources = bundlesSources && _.has(bundlesSources, statAsset.name) ? bundlesSources[statAsset.name] : null;
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 = gzipSize.sync(assetSources.src);
119
- } // Picking modules from current bundle script
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
- const assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule)); // Adding parsed sources
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
- } // Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
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
- _.pullAll(assetModules, unparsedEntryModules);
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
- var _chunkToInitialByEntr;
165
-
166
- return {
167
- label: filename,
168
- isAsset: true,
169
- // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
170
- // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
171
- // be the size of minified bundle.
172
- // Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
173
- statSize: asset.tree.size || asset.size,
174
- parsedSize: asset.parsedSize,
175
- gzipSize: asset.gzipSize,
176
- groups: _.invokeMap(asset.tree.children, 'toChartData'),
177
- isInitialByEntrypoint: (_chunkToInitialByEntr = chunkToInitialByEntrypoint[filename]) !== null && _chunkToInitialByEntr !== void 0 ? _chunkToInitialByEntr : {}
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 => _(c.assetsByChunkName).values().flatten().includes(assetName));
181
+ return flatten((bundleStats.children || []).find(c => Object.values(c.assetsByChunkName))).includes(assetName);
190
182
  }
191
-
192
183
  function getBundleModules(bundleStats) {
193
- return _(bundleStats.chunks).map('modules').concat(bundleStats.modules).compact().flatten().uniqBy('id') // Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
194
- .reject(isRuntimeModule).value();
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
- function createModulesTree(modules) {
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
- var _chunkToEntrypointIni;
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
+ }