webpack-bundle-analyzer 3.6.0 → 3.9.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.
@@ -5,14 +5,15 @@ const {bold} = require('chalk');
5
5
 
6
6
  const Logger = require('./Logger');
7
7
  const viewer = require('./viewer');
8
+ const utils = require('./utils');
8
9
 
9
10
  class BundleAnalyzerPlugin {
10
-
11
11
  constructor(opts = {}) {
12
12
  this.opts = {
13
13
  analyzerMode: 'server',
14
14
  analyzerHost: '127.0.0.1',
15
- reportFilename: 'report.html',
15
+ reportFilename: null,
16
+ reportTitle: utils.defaultTitle,
16
17
  defaultSizes: 'parsed',
17
18
  openAnalyzer: true,
18
19
  generateStatsFile: false,
@@ -51,6 +52,8 @@ class BundleAnalyzerPlugin {
51
52
  actions.push(() => this.startAnalyzerServer(stats.toJson()));
52
53
  } else if (this.opts.analyzerMode === 'static') {
53
54
  actions.push(() => this.generateStaticReport(stats.toJson()));
55
+ } else if (this.opts.analyzerMode === 'json') {
56
+ actions.push(() => this.generateJSONReport(stats.toJson()));
54
57
  }
55
58
 
56
59
  if (actions.length) {
@@ -107,6 +110,7 @@ class BundleAnalyzerPlugin {
107
110
  openBrowser: this.opts.openAnalyzer,
108
111
  host: this.opts.analyzerHost,
109
112
  port: this.opts.analyzerPort,
113
+ reportTitle: this.opts.reportTitle,
110
114
  bundleDir: this.getBundleDirFromCompiler(),
111
115
  logger: this.logger,
112
116
  defaultSizes: this.opts.defaultSizes,
@@ -115,10 +119,20 @@ class BundleAnalyzerPlugin {
115
119
  }
116
120
  }
117
121
 
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
+ });
129
+ }
130
+
118
131
  async generateStaticReport(stats) {
119
132
  await viewer.generateReport(stats, {
120
133
  openBrowser: this.opts.openAnalyzer,
121
- reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename),
134
+ reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'),
135
+ reportTitle: this.opts.reportTitle,
122
136
  bundleDir: this.getBundleDirFromCompiler(),
123
137
  logger: this.logger,
124
138
  defaultSizes: this.opts.defaultSizes,
package/src/analyzer.js CHANGED
@@ -27,7 +27,24 @@ function getViewerData(bundleStats, bundleDir, opts) {
27
27
 
28
28
  // Sometimes all the information is located in `children` array (e.g. problem in #10)
29
29
  if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
30
+ const {children} = bundleStats;
30
31
  bundleStats = bundleStats.children[0];
32
+ // Sometimes if there are additional child chunks produced add them as child assets,
33
+ // leave the 1st one as that is considered the 'root' asset.
34
+ for (let i = 1; i < children.length; i++) {
35
+ bundleStats.children[i].assets.forEach((asset) => {
36
+ asset.isChild = true;
37
+ bundleStats.assets.push(asset);
38
+ });
39
+ }
40
+ } else if (!_.isEmpty(bundleStats.children)) {
41
+ // Sometimes if there are additional child chunks produced add them as child assets
42
+ bundleStats.children.forEach((child) => {
43
+ child.assets.forEach((asset) => {
44
+ asset.isChild = true;
45
+ bundleStats.assets.push(asset);
46
+ });
47
+ });
31
48
  }
32
49
 
33
50
  // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
@@ -70,8 +87,10 @@ function getViewerData(bundleStats, bundleDir, opts) {
70
87
  }
71
88
  }
72
89
 
73
- const modules = getBundleModules(bundleStats);
74
90
  const assets = _.transform(bundleStats.assets, (result, statAsset) => {
91
+ // If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
92
+ const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
93
+ const modules = assetBundles ? getBundleModules(assetBundles) : [];
75
94
  const asset = result[statAsset.name] = _.pick(statAsset, 'size');
76
95
 
77
96
  if (bundlesSources && _.has(bundlesSources, statAsset.name)) {
@@ -113,6 +132,15 @@ function readStatsFromFile(filename) {
113
132
  );
114
133
  }
115
134
 
135
+ function getChildAssetBundles(bundleStats, assetName) {
136
+ return _.find(bundleStats.children, (c) =>
137
+ _(c.assetsByChunkName)
138
+ .values()
139
+ .flatten()
140
+ .includes(assetName)
141
+ );
142
+ }
143
+
116
144
  function getBundleModules(bundleStats) {
117
145
  return _(bundleStats.chunks)
118
146
  .map('modules')
@@ -9,6 +9,7 @@ const {magenta} = require('chalk');
9
9
  const analyzer = require('../analyzer');
10
10
  const viewer = require('../viewer');
11
11
  const Logger = require('../Logger');
12
+ const utils = require('../utils');
12
13
 
13
14
  const SIZES = new Set(['stat', 'parsed', 'gzip']);
14
15
 
@@ -26,9 +27,10 @@ const program = commander
26
27
  )
27
28
  .option(
28
29
  '-m, --mode <mode>',
29
- 'Analyzer mode. Should be `server` or `static`.' +
30
- br('In `server` mode analyzer will start HTTP server to show bundle report.') +
31
- br('In `static` mode single HTML file with bundle report will be generated.'),
30
+ 'Analyzer mode. Should be `server`,`static` or `json`.' +
31
+ br('In `server` mode analyzer will start HTTP server to show bundle report.') +
32
+ br('In `static` mode single HTML file with bundle report will be generated.') +
33
+ br('In `json` mode single JSON file with bundle report will be generated.'),
32
34
  'server'
33
35
  )
34
36
  .option(
@@ -45,8 +47,11 @@ const program = commander
45
47
  )
46
48
  .option(
47
49
  '-r, --report <file>',
48
- 'Path to bundle report file that will be generated in `static` mode.',
49
- 'report.html'
50
+ 'Path to bundle report file that will be generated in `static` mode.'
51
+ )
52
+ .option(
53
+ '-t, --title <title>',
54
+ 'String to use in title element of html report.'
50
55
  )
51
56
  .option(
52
57
  '-s, --default-sizes <type>',
@@ -77,6 +82,7 @@ let {
77
82
  host,
78
83
  port,
79
84
  report: reportFilename,
85
+ title: reportTitle,
80
86
  defaultSizes,
81
87
  logLevel,
82
88
  open: openBrowser,
@@ -85,8 +91,14 @@ let {
85
91
  } = program;
86
92
  const logger = new Logger(logLevel);
87
93
 
94
+ if (typeof reportTitle === 'undefined') {
95
+ reportTitle = utils.defaultTitle;
96
+ }
97
+
88
98
  if (!bundleStatsFile) showHelp('Provide path to Webpack Stats file as first argument');
89
- if (mode !== 'server' && mode !== 'static') showHelp('Invalid mode. Should be either `server` or `static`.');
99
+ if (mode !== 'server' && mode !== 'static' && mode !== 'json') {
100
+ showHelp('Invalid mode. Should be either `server`, `static` or `json`.');
101
+ }
90
102
  if (mode === 'server') {
91
103
  if (!host) showHelp('Invalid host name');
92
104
 
@@ -114,19 +126,28 @@ if (mode === 'server') {
114
126
  port,
115
127
  host,
116
128
  defaultSizes,
129
+ reportTitle,
117
130
  bundleDir,
118
131
  excludeAssets,
119
132
  logger: new Logger(logLevel)
120
133
  });
121
- } else {
134
+ } else if (mode === 'static') {
122
135
  viewer.generateReport(bundleStats, {
123
136
  openBrowser,
124
- reportFilename: resolve(reportFilename),
137
+ reportFilename: resolve(reportFilename || 'report.html'),
138
+ reportTitle,
125
139
  defaultSizes,
126
140
  bundleDir,
127
141
  excludeAssets,
128
142
  logger: new Logger(logLevel)
129
143
  });
144
+ } else if (mode === 'json') {
145
+ viewer.generateJSONReport(bundleStats, {
146
+ reportFilename: resolve(reportFilename || 'report.json'),
147
+ bundleDir,
148
+ excludeAssets,
149
+ logger: new Logger(logLevel)
150
+ });
130
151
  }
131
152
 
132
153
  function showHelp(error) {
package/src/parseUtils.js CHANGED
@@ -25,6 +25,22 @@ function parseBundle(bundlePath) {
25
25
  ast,
26
26
  walkState,
27
27
  {
28
+ AssignmentExpression(node, state) {
29
+ if (state.locations) return;
30
+
31
+ // Modules are stored in exports.modules:
32
+ // exports.modules = {};
33
+ const {left, right} = node;
34
+
35
+ if (
36
+ left &&
37
+ left.object && left.object.name === 'exports' &&
38
+ left.property && left.property.name === 'modules' &&
39
+ isModulesHash(right)
40
+ ) {
41
+ state.locations = getModulesLocations(right);
42
+ }
43
+ },
28
44
  CallExpression(node, state, c) {
29
45
  if (state.locations) return;
30
46
 
@@ -63,6 +79,15 @@ function parseBundle(bundlePath) {
63
79
  return;
64
80
  }
65
81
 
82
+ // Webpack v4 WebWorkerChunkTemplatePlugin
83
+ // globalObject.chunkCallbackName([<chunks>],<modules>, ...);
84
+ // Both globalObject and chunkCallbackName can be changed through the config, so we can't check them.
85
+ if (isAsyncWebWorkerChunkExpression(node)) {
86
+ state.locations = getModulesLocations(args[1]);
87
+ return;
88
+ }
89
+
90
+
66
91
  // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
67
92
  // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
68
93
  _.each(args, arg => c(arg, state));
@@ -182,14 +207,6 @@ function isAsyncChunkPushExpression(node) {
182
207
  callee.type === 'MemberExpression' &&
183
208
  callee.property.name === 'push' &&
184
209
  callee.object.type === 'AssignmentExpression' &&
185
- callee.object.left.object &&
186
- (
187
- callee.object.left.object.name === 'window' ||
188
- // `self` is a common output.globalObject value used to support both workers and browsers
189
- callee.object.left.object.name === 'self' ||
190
- // Webpack 4 uses `this` instead of `window`
191
- callee.object.left.object.type === 'ThisExpression'
192
- ) &&
193
210
  args.length === 1 &&
194
211
  args[0].type === 'ArrayExpression' &&
195
212
  mayBeAsyncChunkArguments(args[0].elements) &&
@@ -204,6 +221,18 @@ function mayBeAsyncChunkArguments(args) {
204
221
  );
205
222
  }
206
223
 
224
+ function isAsyncWebWorkerChunkExpression(node) {
225
+ const {callee, type, arguments: args} = node;
226
+
227
+ return (
228
+ type === 'CallExpression' &&
229
+ callee.type === 'MemberExpression' &&
230
+ args.length === 2 &&
231
+ isChunkIds(args[0]) &&
232
+ isModulesList(args[1])
233
+ );
234
+ }
235
+
207
236
  function getModulesLocations(node) {
208
237
  if (node.type === 'ObjectExpression') {
209
238
  // Modules hash
package/src/utils.js CHANGED
@@ -39,12 +39,15 @@ function createAssetsFilter(excludePatterns) {
39
39
  * @desc get string of current time
40
40
  * format: dd/MMM HH:mm
41
41
  * */
42
- exports.getCurrentTime = function () {
42
+ exports.defaultTitle = function () {
43
43
  const time = new Date();
44
44
  const year = time.getFullYear();
45
45
  const month = MONTHS[time.getMonth()];
46
46
  const day = time.getDate();
47
- const hour = time.getHours();
48
- const minute = time.getMinutes();
49
- return `${day} ${month} ${year} at ${hour}:${minute}`;
47
+ const hour = `0${time.getHours()}`.slice(-2);
48
+ const minute = `0${time.getMinutes()}`.slice(-2);
49
+
50
+ const currentTime = `${day} ${month} ${year} at ${hour}:${minute}`;
51
+
52
+ return `${process.env.npm_package_name || 'Webpack Bundle Analyzer'} [${currentTime}]`;
50
53
  };
package/src/viewer.js CHANGED
@@ -10,22 +10,28 @@ const opener = require('opener');
10
10
  const mkdir = require('mkdirp');
11
11
  const {bold} = require('chalk');
12
12
 
13
- const utils = require('./utils');
14
13
  const Logger = require('./Logger');
15
14
  const analyzer = require('./analyzer');
16
15
 
17
16
  const projectRoot = path.resolve(__dirname, '..');
18
17
  const assetsRoot = path.join(projectRoot, 'public');
19
18
 
19
+ function resolveTitle(reportTitle) {
20
+ if (typeof reportTitle === 'function') {
21
+ return reportTitle();
22
+ } else {
23
+ return reportTitle;
24
+ }
25
+ }
26
+
20
27
  module.exports = {
21
28
  startServer,
22
29
  generateReport,
30
+ generateJSONReport,
23
31
  // deprecated
24
32
  start: startServer
25
33
  };
26
34
 
27
- const title = `${process.env.npm_package_name || 'Webpack Bundle Analyzer'} [${utils.getCurrentTime()}]`;
28
-
29
35
  async function startServer(bundleStats, opts) {
30
36
  const {
31
37
  port = 8888,
@@ -34,7 +40,8 @@ async function startServer(bundleStats, opts) {
34
40
  bundleDir = null,
35
41
  logger = new Logger(),
36
42
  defaultSizes = 'parsed',
37
- excludeAssets = null
43
+ excludeAssets = null,
44
+ reportTitle
38
45
  } = opts || {};
39
46
 
40
47
  const analyzerOpts = {logger, excludeAssets};
@@ -55,7 +62,7 @@ async function startServer(bundleStats, opts) {
55
62
  app.use('/', (req, res) => {
56
63
  res.render('viewer', {
57
64
  mode: 'server',
58
- title,
65
+ title: resolveTitle(reportTitle),
59
66
  get chartData() { return chartData },
60
67
  defaultSizes,
61
68
  enableWebSocket: true,
@@ -121,7 +128,8 @@ async function startServer(bundleStats, opts) {
121
128
  async function generateReport(bundleStats, opts) {
122
129
  const {
123
130
  openBrowser = true,
124
- reportFilename = 'report.html',
131
+ reportFilename,
132
+ reportTitle,
125
133
  bundleDir = null,
126
134
  logger = new Logger(),
127
135
  defaultSizes = 'parsed',
@@ -137,7 +145,7 @@ async function generateReport(bundleStats, opts) {
137
145
  `${projectRoot}/views/viewer.ejs`,
138
146
  {
139
147
  mode: 'static',
140
- title,
148
+ title: resolveTitle(reportTitle),
141
149
  chartData,
142
150
  defaultSizes,
143
151
  enableWebSocket: false,
@@ -158,9 +166,7 @@ async function generateReport(bundleStats, opts) {
158
166
  mkdir.sync(path.dirname(reportFilepath));
159
167
  fs.writeFileSync(reportFilepath, reportHtml);
160
168
 
161
- logger.info(
162
- `${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`
163
- );
169
+ logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
164
170
 
165
171
  if (openBrowser) {
166
172
  opener(`file://${reportFilepath}`);
@@ -174,6 +180,19 @@ async function generateReport(bundleStats, opts) {
174
180
  });
175
181
  }
176
182
 
183
+ async function generateJSONReport(bundleStats, opts) {
184
+ const {reportFilename, bundleDir = null, logger = new Logger(), excludeAssets = null} = opts || {};
185
+
186
+ const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir);
187
+
188
+ if (!chartData) return;
189
+
190
+ mkdir.sync(path.dirname(reportFilename));
191
+ fs.writeFileSync(reportFilename, JSON.stringify(chartData));
192
+
193
+ logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`);
194
+ }
195
+
177
196
  function getAssetContent(filename) {
178
197
  const assetPath = path.join(assetsRoot, filename);
179
198