webpack-bundle-analyzer 3.9.0 → 4.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/CHANGELOG.md CHANGED
@@ -14,6 +14,35 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
14
14
 
15
15
  <!-- Add changelog entries for new changes under this section -->
16
16
 
17
+ ## 4.2.0
18
+
19
+ * **Improvement**
20
+ * A number of improvements to reduce the number of dependencies ([#391](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/391), [#396](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/396), [#397](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/397))
21
+
22
+ * **Bug Fix**
23
+ * Prevent crashes for bundles generated from webpack array configs. ([#394](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/394) by [@ctavan](https://github.com/ctavan))
24
+ * Fix `non-asset` assets causing analyze failure. ([#385](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/385) by [@ZKHelloworld](https://github.com/ZKHelloworld))
25
+
26
+ ## 4.1.0
27
+
28
+ * **Improvement**
29
+ * Significantly speed up generation of `stats.json` file (see `generateStatsFile` option).
30
+
31
+ ## 4.0.0
32
+
33
+ * **Breaking change**
34
+ * Dropped support for Node.js 6 and 8. Minimal required version now is v10.13.0
35
+
36
+ * **Improvement**
37
+ * Support for Webpack 5
38
+
39
+ * **Bug Fix**
40
+ * Prevent crashes when `openAnalyzer` was set to true in environments where there's no program to handle opening. ([#382](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/382) by [@wbobeirne](https://github.com/wbobeirne))
41
+
42
+ * **Internal**
43
+ * Updated dependencies
44
+ * Added support for multiple Webpack versions in tests
45
+
17
46
  ## 3.9.0
18
47
 
19
48
  * **New Feature**
@@ -1,21 +1,9 @@
1
1
  "use strict";
2
2
 
3
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
4
-
5
- function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
6
-
7
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
8
-
9
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
10
-
11
- function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
12
-
13
- const bfj = require('bfj');
3
+ const fs = require('fs');
14
4
 
15
5
  const path = require('path');
16
6
 
17
- const mkdir = require('mkdirp');
18
-
19
7
  const {
20
8
  bold
21
9
  } = require('chalk');
@@ -26,9 +14,13 @@ const viewer = require('./viewer');
26
14
 
27
15
  const utils = require('./utils');
28
16
 
17
+ const {
18
+ writeStats
19
+ } = require('./statsUtils');
20
+
29
21
  class BundleAnalyzerPlugin {
30
22
  constructor(opts = {}) {
31
- this.opts = _objectSpread({
23
+ this.opts = {
32
24
  analyzerMode: 'server',
33
25
  analyzerHost: '127.0.0.1',
34
26
  reportFilename: null,
@@ -41,10 +33,10 @@ class BundleAnalyzerPlugin {
41
33
  excludeAssets: null,
42
34
  logLevel: 'info',
43
35
  // deprecated
44
- startAnalyzer: true
45
- }, opts, {
36
+ startAnalyzer: true,
37
+ ...opts,
46
38
  analyzerPort: 'analyzerPort' in opts ? opts.analyzerPort === 'auto' ? 0 : opts.analyzerPort : 8888
47
- });
39
+ };
48
40
  this.server = null;
49
41
  this.logger = new Logger(this.opts.logLevel);
50
42
  }
@@ -76,14 +68,14 @@ class BundleAnalyzerPlugin {
76
68
 
77
69
  if (actions.length) {
78
70
  // Making analyzer logs to be after all webpack logs in the console
79
- setImmediate( /*#__PURE__*/_asyncToGenerator(function* () {
71
+ setImmediate(async () => {
80
72
  try {
81
- yield Promise.all(actions.map(action => action()));
73
+ await Promise.all(actions.map(action => action()));
82
74
  callback();
83
75
  } catch (e) {
84
76
  callback(e);
85
77
  }
86
- }));
78
+ });
87
79
  } else {
88
80
  callback();
89
81
  }
@@ -96,78 +88,56 @@ class BundleAnalyzerPlugin {
96
88
  }
97
89
  }
98
90
 
99
- generateStatsFile(stats) {
100
- var _this = this;
101
-
102
- return _asyncToGenerator(function* () {
103
- const statsFilepath = path.resolve(_this.compiler.outputPath, _this.opts.statsFilename);
104
- mkdir.sync(path.dirname(statsFilepath));
105
-
106
- try {
107
- yield bfj.write(statsFilepath, stats, {
108
- space: 2,
109
- promises: 'ignore',
110
- buffers: 'ignore',
111
- maps: 'ignore',
112
- iterables: 'ignore',
113
- circular: 'ignore'
114
- });
91
+ async generateStatsFile(stats) {
92
+ const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename);
93
+ await fs.promises.mkdir(path.dirname(statsFilepath), {
94
+ recursive: true
95
+ });
115
96
 
116
- _this.logger.info(`${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`);
117
- } catch (error) {
118
- _this.logger.error(`${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}`);
119
- }
120
- })();
97
+ try {
98
+ await writeStats(stats, statsFilepath);
99
+ this.logger.info(`${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`);
100
+ } catch (error) {
101
+ this.logger.error(`${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}`);
102
+ }
121
103
  }
122
104
 
123
- startAnalyzerServer(stats) {
124
- var _this2 = this;
125
-
126
- return _asyncToGenerator(function* () {
127
- if (_this2.server) {
128
- (yield _this2.server).updateChartData(stats);
129
- } else {
130
- _this2.server = viewer.startServer(stats, {
131
- openBrowser: _this2.opts.openAnalyzer,
132
- host: _this2.opts.analyzerHost,
133
- port: _this2.opts.analyzerPort,
134
- reportTitle: _this2.opts.reportTitle,
135
- bundleDir: _this2.getBundleDirFromCompiler(),
136
- logger: _this2.logger,
137
- defaultSizes: _this2.opts.defaultSizes,
138
- excludeAssets: _this2.opts.excludeAssets
139
- });
140
- }
141
- })();
105
+ async startAnalyzerServer(stats) {
106
+ if (this.server) {
107
+ (await this.server).updateChartData(stats);
108
+ } else {
109
+ this.server = viewer.startServer(stats, {
110
+ openBrowser: this.opts.openAnalyzer,
111
+ host: this.opts.analyzerHost,
112
+ port: this.opts.analyzerPort,
113
+ reportTitle: this.opts.reportTitle,
114
+ bundleDir: this.getBundleDirFromCompiler(),
115
+ logger: this.logger,
116
+ defaultSizes: this.opts.defaultSizes,
117
+ excludeAssets: this.opts.excludeAssets
118
+ });
119
+ }
142
120
  }
143
121
 
144
- generateJSONReport(stats) {
145
- var _this3 = this;
146
-
147
- return _asyncToGenerator(function* () {
148
- yield viewer.generateJSONReport(stats, {
149
- reportFilename: path.resolve(_this3.compiler.outputPath, _this3.opts.reportFilename || 'report.json'),
150
- bundleDir: _this3.getBundleDirFromCompiler(),
151
- logger: _this3.logger,
152
- excludeAssets: _this3.opts.excludeAssets
153
- });
154
- })();
122
+ async generateJSONReport(stats) {
123
+ await viewer.generateJSONReport(stats, {
124
+ reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'),
125
+ bundleDir: this.getBundleDirFromCompiler(),
126
+ logger: this.logger,
127
+ excludeAssets: this.opts.excludeAssets
128
+ });
155
129
  }
156
130
 
157
- generateStaticReport(stats) {
158
- var _this4 = this;
159
-
160
- return _asyncToGenerator(function* () {
161
- yield viewer.generateReport(stats, {
162
- openBrowser: _this4.opts.openAnalyzer,
163
- reportFilename: path.resolve(_this4.compiler.outputPath, _this4.opts.reportFilename || 'report.html'),
164
- reportTitle: _this4.opts.reportTitle,
165
- bundleDir: _this4.getBundleDirFromCompiler(),
166
- logger: _this4.logger,
167
- defaultSizes: _this4.opts.defaultSizes,
168
- excludeAssets: _this4.opts.excludeAssets
169
- });
170
- })();
131
+ async generateStaticReport(stats) {
132
+ await viewer.generateReport(stats, {
133
+ openBrowser: this.opts.openAnalyzer,
134
+ reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'),
135
+ reportTitle: this.opts.reportTitle,
136
+ bundleDir: this.getBundleDirFromCompiler(),
137
+ logger: this.logger,
138
+ defaultSizes: this.opts.defaultSizes,
139
+ excludeAssets: this.opts.excludeAssets
140
+ });
171
141
  }
172
142
 
173
143
  getBundleDirFromCompiler() {
package/lib/analyzer.js CHANGED
@@ -42,7 +42,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
42
42
  // leave the 1st one as that is considered the 'root' asset.
43
43
 
44
44
  for (let i = 1; i < children.length; i++) {
45
- bundleStats.children[i].assets.forEach(asset => {
45
+ children[i].assets.forEach(asset => {
46
46
  asset.isChild = true;
47
47
  bundleStats.assets.push(asset);
48
48
  });
@@ -58,9 +58,14 @@ function getViewerData(bundleStats, bundleDir, opts) {
58
58
  } // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
59
59
 
60
60
 
61
- bundleStats.assets = _.filter(bundleStats.assets, asset => {
62
- // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
61
+ bundleStats.assets = bundleStats.assets.filter(asset => {
62
+ // Filter out non 'asset' type asset if type is provided (Webpack 5 add a type to indicate asset types)
63
+ if (asset.type && asset.type !== 'asset') {
64
+ return false;
65
+ } // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
63
66
  // See #22
67
+
68
+
64
69
  asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
65
70
  return FILENAME_EXTENSIONS.test(asset.name) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
66
71
  }); // Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
@@ -84,9 +89,8 @@ function getViewerData(bundleStats, bundleDir, opts) {
84
89
  continue;
85
90
  }
86
91
 
87
- bundlesSources[statAsset.name] = bundleInfo.src;
88
-
89
- _.assign(parsedModules, bundleInfo.modules);
92
+ bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
93
+ Object.assign(parsedModules, bundleInfo.modules);
90
94
  }
91
95
 
92
96
  if (_.isEmpty(bundlesSources)) {
@@ -96,41 +100,73 @@ function getViewerData(bundleStats, bundleDir, opts) {
96
100
  }
97
101
  }
98
102
 
99
- const assets = _.transform(bundleStats.assets, (result, statAsset) => {
103
+ const assets = bundleStats.assets.reduce((result, statAsset) => {
100
104
  // If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
101
105
  const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
102
106
  const modules = assetBundles ? getBundleModules(assetBundles) : [];
103
107
 
104
108
  const asset = result[statAsset.name] = _.pick(statAsset, 'size');
105
109
 
106
- if (bundlesSources && _.has(bundlesSources, statAsset.name)) {
107
- asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name]);
108
- asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]);
110
+ const assetSources = bundlesSources && _.has(bundlesSources, statAsset.name) ? bundlesSources[statAsset.name] : null;
111
+
112
+ if (assetSources) {
113
+ asset.parsedSize = Buffer.byteLength(assetSources.src);
114
+ asset.gzipSize = gzipSize.sync(assetSources.src);
109
115
  } // Picking modules from current bundle script
110
116
 
111
117
 
112
- asset.modules = _(modules).filter(statModule => assetHasModule(statAsset, statModule)).each(statModule => {
113
- if (parsedModules) {
114
- statModule.parsedSrc = parsedModules[statModule.id];
118
+ const assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule)); // Adding parsed sources
119
+
120
+ if (parsedModules) {
121
+ const unparsedEntryModules = [];
122
+
123
+ for (const statModule of assetModules) {
124
+ if (parsedModules[statModule.id]) {
125
+ statModule.parsedSrc = parsedModules[statModule.id];
126
+ } else if (isEntryModule(statModule)) {
127
+ unparsedEntryModules.push(statModule);
128
+ }
129
+ } // Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
130
+ // Because of this they basically become a concatenated module, for which we can't even precisely determine its
131
+ // parsed source as it's located in the same scope as all Webpack runtime helpers.
132
+
133
+
134
+ if (unparsedEntryModules.length && assetSources) {
135
+ if (unparsedEntryModules.length === 1) {
136
+ // So if there is only one entry we consider its parsed source to be all the bundle code excluding code
137
+ // from parsed modules.
138
+ unparsedEntryModules[0].parsedSrc = assetSources.runtimeSrc;
139
+ } else {
140
+ // If there are multiple entry points we move all of them under synthetic concatenated module.
141
+ _.pullAll(assetModules, unparsedEntryModules);
142
+
143
+ assetModules.unshift({
144
+ identifier: './entry modules',
145
+ name: './entry modules',
146
+ modules: unparsedEntryModules,
147
+ size: unparsedEntryModules.reduce((totalSize, module) => totalSize + module.size, 0),
148
+ parsedSrc: assetSources.runtimeSrc
149
+ });
150
+ }
115
151
  }
116
- });
152
+ }
153
+
154
+ asset.modules = assetModules;
117
155
  asset.tree = createModulesTree(asset.modules);
156
+ return result;
118
157
  }, {});
119
-
120
- return _.transform(assets, (result, asset, filename) => {
121
- result.push({
122
- label: filename,
123
- isAsset: true,
124
- // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
125
- // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
126
- // be the size of minified bundle.
127
- // Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
128
- statSize: asset.tree.size || asset.size,
129
- parsedSize: asset.parsedSize,
130
- gzipSize: asset.gzipSize,
131
- groups: _.invokeMap(asset.tree.children, 'toChartData')
132
- });
133
- }, []);
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
+ groups: _.invokeMap(asset.tree.children, 'toChartData')
169
+ }));
134
170
  }
135
171
 
136
172
  function readStatsFromFile(filename) {
@@ -138,23 +174,30 @@ function readStatsFromFile(filename) {
138
174
  }
139
175
 
140
176
  function getChildAssetBundles(bundleStats, assetName) {
141
- return _.find(bundleStats.children, c => _(c.assetsByChunkName).values().flatten().includes(assetName));
177
+ return (bundleStats.children || []).find(c => _(c.assetsByChunkName).values().flatten().includes(assetName));
142
178
  }
143
179
 
144
180
  function getBundleModules(bundleStats) {
145
- return _(bundleStats.chunks).map('modules').concat(bundleStats.modules).compact().flatten().uniqBy('id').value();
181
+ 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)
182
+ .reject(isRuntimeModule).value();
146
183
  }
147
184
 
148
185
  function assetHasModule(statAsset, statModule) {
149
186
  // Checking if this module is the part of asset chunks
150
- return _.some(statModule.chunks, moduleChunk => _.includes(statAsset.chunks, moduleChunk));
187
+ return statModule.chunks.some(moduleChunk => statAsset.chunks.includes(moduleChunk));
151
188
  }
152
189
 
153
- function createModulesTree(modules) {
154
- const root = new Folder('.');
190
+ function isEntryModule(statModule) {
191
+ return statModule.depth === 0;
192
+ }
155
193
 
156
- _.each(modules, module => root.addModule(module));
194
+ function isRuntimeModule(statModule) {
195
+ return statModule.moduleType === 'runtime';
196
+ }
157
197
 
198
+ function createModulesTree(modules) {
199
+ const root = new Folder('.');
200
+ modules.forEach(module => root.addModule(module));
158
201
  root.mergeNestedFolders();
159
202
  return root;
160
203
  }
@@ -6,8 +6,6 @@ const {
6
6
  dirname
7
7
  } = require('path');
8
8
 
9
- const _ = require('lodash');
10
-
11
9
  const commander = require('commander');
12
10
 
13
11
  const {
@@ -113,7 +111,7 @@ function showHelp(error) {
113
111
  }
114
112
 
115
113
  function br(str) {
116
- return `\n${_.repeat(' ', 28)}${str}`;
114
+ return `\n${' '.repeat(28)}${str}`;
117
115
  }
118
116
 
119
117
  function array() {
package/lib/parseUtils.js CHANGED
@@ -22,9 +22,45 @@ function parseBundle(bundlePath) {
22
22
  ecmaVersion: 2050
23
23
  });
24
24
  const walkState = {
25
- locations: null
25
+ locations: null,
26
+ expressionStatementDepth: 0
26
27
  };
27
28
  walk.recursive(ast, walkState, {
29
+ ExpressionStatement(node, state, c) {
30
+ if (state.locations) return;
31
+ state.expressionStatementDepth++;
32
+
33
+ if ( // Webpack 5 stores modules in the the top-level IIFE
34
+ state.expressionStatementDepth === 1 && ast.body.includes(node) && isIIFE(node)) {
35
+ const fn = getIIFECallExpression(node);
36
+
37
+ if ( // It should not contain neither arguments
38
+ fn.arguments.length === 0 && // ...nor parameters
39
+ fn.callee.params.length === 0) {
40
+ // Modules are stored in the very first variable declaration as hash
41
+ const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === 'VariableDeclaration');
42
+
43
+ if (firstVariableDeclaration) {
44
+ for (const declaration of firstVariableDeclaration.declarations) {
45
+ if (declaration.init) {
46
+ state.locations = getModulesLocations(declaration.init);
47
+
48
+ if (state.locations) {
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ if (!state.locations) {
58
+ c(node.expression, state);
59
+ }
60
+
61
+ state.expressionStatementDepth--;
62
+ },
63
+
28
64
  AssignmentExpression(node, state) {
29
65
  if (state.locations) return; // Modules are stored in exports.modules:
30
66
  // exports.modules = {};
@@ -76,7 +112,7 @@ function parseBundle(bundlePath) {
76
112
  // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
77
113
 
78
114
 
79
- _.each(args, arg => c(arg, state));
115
+ args.forEach(arg => c(arg, state));
80
116
  }
81
117
 
82
118
  });
@@ -89,10 +125,43 @@ function parseBundle(bundlePath) {
89
125
  }
90
126
 
91
127
  return {
128
+ modules,
92
129
  src: content,
93
- modules
130
+ runtimeSrc: getBundleRuntime(content, walkState.locations)
94
131
  };
95
132
  }
133
+ /**
134
+ * Returns bundle source except modules
135
+ */
136
+
137
+
138
+ function getBundleRuntime(content, modulesLocations) {
139
+ const sortedLocations = Object.values(modulesLocations || {}).sort((a, b) => a.start - b.start);
140
+ let result = '';
141
+ let lastIndex = 0;
142
+
143
+ for (const {
144
+ start,
145
+ end
146
+ } of sortedLocations) {
147
+ result += content.slice(lastIndex, start);
148
+ lastIndex = end;
149
+ }
150
+
151
+ return result + content.slice(lastIndex, content.length);
152
+ }
153
+
154
+ function isIIFE(node) {
155
+ return node.type === 'ExpressionStatement' && (node.expression.type === 'CallExpression' || node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression');
156
+ }
157
+
158
+ function getIIFECallExpression(node) {
159
+ if (node.expression.type === 'UnaryExpression') {
160
+ return node.expression.argument;
161
+ } else {
162
+ return node.expression;
163
+ }
164
+ }
96
165
 
97
166
  function isModulesList(node) {
98
167
  return isSimpleModulesList(node) || // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
@@ -107,11 +176,11 @@ function isSimpleModulesList(node) {
107
176
  }
108
177
 
109
178
  function isModulesHash(node) {
110
- return node.type === 'ObjectExpression' && _(node.properties).map('value').every(isModuleWrapper);
179
+ return node.type === 'ObjectExpression' && node.properties.map(node => node.value).every(isModuleWrapper);
111
180
  }
112
181
 
113
182
  function isModulesArray(node) {
114
- return node.type === 'ArrayExpression' && _.every(node.elements, elem => // Some of array items may be skipped because there is no module with such id
183
+ return node.type === 'ArrayExpression' && node.elements.every(elem => // Some of array items may be skipped because there is no module with such id
115
184
  !elem || isModuleWrapper(elem));
116
185
  }
117
186
 
@@ -143,7 +212,7 @@ function isNumericId(node) {
143
212
 
144
213
  function isChunkIds(node) {
145
214
  // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
146
- return node.type === 'ArrayExpression' && _.every(node.elements, isModuleId);
215
+ return node.type === 'ArrayExpression' && node.elements.every(isModuleId);
147
216
  }
148
217
 
149
218
  function isAsyncChunkPushExpression(node) {
@@ -171,9 +240,10 @@ function getModulesLocations(node) {
171
240
  if (node.type === 'ObjectExpression') {
172
241
  // Modules hash
173
242
  const modulesNodes = node.properties;
174
- return _.transform(modulesNodes, (result, moduleNode) => {
243
+ return modulesNodes.reduce((result, moduleNode) => {
175
244
  const moduleId = moduleNode.key.name || moduleNode.key.value;
176
245
  result[moduleId] = getModuleLocation(moduleNode.value);
246
+ return result;
177
247
  }, {});
178
248
  }
179
249
 
@@ -186,9 +256,12 @@ function getModulesLocations(node) {
186
256
  0;
187
257
  const modulesNodes = isOptimizedArray ? // The modules reside in the `concat()` function call arguments
188
258
  node.arguments[0].elements : node.elements;
189
- return _.transform(modulesNodes, (result, moduleNode, i) => {
190
- if (!moduleNode) return;
191
- result[i + minId] = getModuleLocation(moduleNode);
259
+ return modulesNodes.reduce((result, moduleNode, i) => {
260
+ if (moduleNode) {
261
+ result[i + minId] = getModuleLocation(moduleNode);
262
+ }
263
+
264
+ return result;
192
265
  }, {});
193
266
  }
194
267
 
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ const {
4
+ createWriteStream
5
+ } = require('fs');
6
+
7
+ const {
8
+ Readable
9
+ } = require('stream');
10
+
11
+ class StatsSerializeStream extends Readable {
12
+ constructor(stats) {
13
+ super();
14
+ this._indentLevel = 0;
15
+ this._stringifier = this._stringify(stats);
16
+ }
17
+
18
+ get _indent() {
19
+ return ' '.repeat(this._indentLevel);
20
+ }
21
+
22
+ _read() {
23
+ let readMore = true;
24
+
25
+ while (readMore) {
26
+ const {
27
+ value,
28
+ done
29
+ } = this._stringifier.next();
30
+
31
+ if (done) {
32
+ this.push(null);
33
+ readMore = false;
34
+ } else {
35
+ readMore = this.push(value);
36
+ }
37
+ }
38
+ }
39
+
40
+ *_stringify(obj) {
41
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || obj === null) {
42
+ yield JSON.stringify(obj);
43
+ } else if (Array.isArray(obj)) {
44
+ yield '[';
45
+ this._indentLevel++;
46
+ let isFirst = true;
47
+
48
+ for (let item of obj) {
49
+ if (item === undefined) {
50
+ item = null;
51
+ }
52
+
53
+ yield `${isFirst ? '' : ','}\n${this._indent}`;
54
+ yield* this._stringify(item);
55
+ isFirst = false;
56
+ }
57
+
58
+ this._indentLevel--;
59
+ yield obj.length ? `\n${this._indent}]` : ']';
60
+ } else {
61
+ yield '{';
62
+ this._indentLevel++;
63
+ let isFirst = true;
64
+ const entries = Object.entries(obj);
65
+
66
+ for (const [itemKey, itemValue] of entries) {
67
+ if (itemValue === undefined) {
68
+ continue;
69
+ }
70
+
71
+ yield `${isFirst ? '' : ','}\n${this._indent}${JSON.stringify(itemKey)}: `;
72
+ yield* this._stringify(itemValue);
73
+ isFirst = false;
74
+ }
75
+
76
+ this._indentLevel--;
77
+ yield entries.length ? `\n${this._indent}}` : '}';
78
+ }
79
+ }
80
+
81
+ }
82
+
83
+ exports.StatsSerializeStream = StatsSerializeStream;
84
+ exports.writeStats = writeStats;
85
+
86
+ async function writeStats(stats, filepath) {
87
+ return new Promise((resolve, reject) => {
88
+ new StatsSerializeStream(stats).on('end', resolve).on('error', reject).pipe(createWriteStream(filepath));
89
+ });
90
+ }