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.
@@ -5,22 +5,19 @@ const {
5
5
  resolve,
6
6
  dirname
7
7
  } = require('path');
8
-
9
8
  const commander = require('commander');
10
-
11
9
  const {
12
10
  magenta
13
- } = require('chalk');
14
-
11
+ } = require('picocolors');
15
12
  const analyzer = require('../analyzer');
16
-
17
13
  const viewer = require('../viewer');
18
-
19
14
  const Logger = require('../Logger');
20
-
21
15
  const utils = require('../utils');
22
-
16
+ const {
17
+ isZstdSupported
18
+ } = require('../sizeUtils');
23
19
  const SIZES = new Set(['stat', 'parsed', 'gzip']);
20
+ const COMPRESSION_ALGORITHMS = new Set(isZstdSupported ? ['gzip', 'brotli', 'zstd'] : ['gzip', 'brotli']);
24
21
  const program = commander.version(require('../../package.json').version).usage(`<bundleStatsFile> [bundleDir] [options]
25
22
 
26
23
  Arguments:
@@ -28,9 +25,10 @@ const program = commander.version(require('../../package.json').version).usage(`
28
25
  bundleStatsFile Path to Webpack Stats JSON file.
29
26
  bundleDir Directory containing all generated bundles.
30
27
  You should provided it if you want analyzer to show you the real parsed module sizes.
31
- By default a directory of stats file is used.`).option('-m, --mode <mode>', 'Analyzer mode. Should be `server`,`static` or `json`.' + br('In `server` mode analyzer will start HTTP server to show bundle report.') + br('In `static` mode single HTML file with bundle report will be generated.') + br('In `json` mode single JSON file with bundle report will be generated.'), 'server').option( // Had to make `host` parameter optional in order to let `-h` flag output help message
32
- // Fixes https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/239
33
- '-h, --host [host]', 'Host that will be used in `server` mode to start HTTP server.', '127.0.0.1').option('-p, --port <n>', 'Port that will be used in `server` mode to start HTTP server.', 8888).option('-r, --report <file>', 'Path to bundle report file that will be generated in `static` mode.').option('-t, --title <title>', 'String to use in title element of html report.').option('-s, --default-sizes <type>', 'Module sizes to show in treemap by default.' + br(`Possible values: ${[...SIZES].join(', ')}`), 'parsed').option('-O, --no-open', "Don't open report in default browser automatically.").option('-e, --exclude <regexp>', 'Assets that should be excluded from the report.' + br('Can be specified multiple times.'), array()).option('-l, --log-level <level>', 'Log level.' + br(`Possible values: ${[...Logger.levels].join(', ')}`), Logger.defaultLevel).parse(process.argv);
28
+ By default a directory of stats file is used.`).option('-m, --mode <mode>', 'Analyzer mode. Should be `server`,`static` or `json`.' + br('In `server` mode analyzer will start HTTP server to show bundle report.') + br('In `static` mode single HTML file with bundle report will be generated.') + br('In `json` mode single JSON file with bundle report will be generated.'), 'server').option(
29
+ // Had to make `host` parameter optional in order to let `-h` flag output help message
30
+ // Fixes https://github.com/webpack/webpack-bundle-analyzer/issues/239
31
+ '-h, --host [host]', 'Host that will be used in `server` mode to start HTTP server.', '127.0.0.1').option('-p, --port <n>', 'Port that will be used in `server` mode to start HTTP server.', 8888).option('-r, --report <file>', 'Path to bundle report file that will be generated in `static` mode.').option('-t, --title <title>', 'String to use in title element of html report.').option('-s, --default-sizes <type>', 'Module sizes to show in treemap by default.' + br(`Possible values: ${[...SIZES].join(', ')}`), 'parsed').option('--compression-algorithm <type>', 'Compression algorithm that will be used to calculate the compressed module sizes.' + br(`Possible values: ${[...COMPRESSION_ALGORITHMS].join(', ')}`), 'gzip').option('-O, --no-open', "Don't open report in default browser automatically.").option('-e, --exclude <regexp>', 'Assets that should be excluded from the report.' + br('Can be specified multiple times.'), array()).option('-l, --log-level <level>', 'Log level.' + br(`Possible values: ${[...Logger.levels].join(', ')}`), Logger.defaultLevel).parse(process.argv);
34
32
  let [bundleStatsFile, bundleDir] = program.args;
35
33
  let {
36
34
  mode,
@@ -39,43 +37,41 @@ let {
39
37
  report: reportFilename,
40
38
  title: reportTitle,
41
39
  defaultSizes,
40
+ compressionAlgorithm,
42
41
  logLevel,
43
42
  open: openBrowser,
44
43
  exclude: excludeAssets
45
44
  } = program.opts();
46
45
  const logger = new Logger(logLevel);
47
-
48
46
  if (typeof reportTitle === 'undefined') {
49
47
  reportTitle = utils.defaultTitle;
50
48
  }
51
-
52
49
  if (!bundleStatsFile) showHelp('Provide path to Webpack Stats file as first argument');
53
-
54
50
  if (mode !== 'server' && mode !== 'static' && mode !== 'json') {
55
51
  showHelp('Invalid mode. Should be either `server`, `static` or `json`.');
56
52
  }
57
-
58
53
  if (mode === 'server') {
59
54
  if (!host) showHelp('Invalid host name');
60
55
  port = port === 'auto' ? 0 : Number(port);
61
56
  if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
62
57
  }
63
-
58
+ if (!COMPRESSION_ALGORITHMS.has(compressionAlgorithm)) {
59
+ showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(', ')}`);
60
+ }
64
61
  if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
65
62
  bundleStatsFile = resolve(bundleStatsFile);
66
63
  if (!bundleDir) bundleDir = dirname(bundleStatsFile);
67
64
  parseAndAnalyse(bundleStatsFile);
68
-
69
65
  async function parseAndAnalyse(bundleStatsFile) {
70
66
  try {
71
67
  const bundleStats = await analyzer.readStatsFromFile(bundleStatsFile);
72
-
73
68
  if (mode === 'server') {
74
69
  viewer.startServer(bundleStats, {
75
70
  openBrowser,
76
71
  port,
77
72
  host,
78
73
  defaultSizes,
74
+ compressionAlgorithm,
79
75
  reportTitle,
80
76
  bundleDir,
81
77
  excludeAssets,
@@ -88,6 +84,7 @@ async function parseAndAnalyse(bundleStatsFile) {
88
84
  reportFilename: resolve(reportFilename || 'report.html'),
89
85
  reportTitle,
90
86
  defaultSizes,
87
+ compressionAlgorithm,
91
88
  bundleDir,
92
89
  excludeAssets,
93
90
  logger: new Logger(logLevel)
@@ -95,6 +92,7 @@ async function parseAndAnalyse(bundleStatsFile) {
95
92
  } else if (mode === 'json') {
96
93
  viewer.generateJSONReport(bundleStats, {
97
94
  reportFilename: resolve(reportFilename || 'report.json'),
95
+ compressionAlgorithm,
98
96
  bundleDir,
99
97
  excludeAssets,
100
98
  logger: new Logger(logLevel)
@@ -106,17 +104,14 @@ async function parseAndAnalyse(bundleStatsFile) {
106
104
  process.exit(1);
107
105
  }
108
106
  }
109
-
110
107
  function showHelp(error) {
111
108
  if (error) console.log(`\n ${magenta(error)}\n`);
112
109
  program.outputHelp();
113
110
  process.exit(1);
114
111
  }
115
-
116
112
  function br(str) {
117
- return `\n${' '.repeat(28)}${str}`;
113
+ return `\n${' '.repeat(32)}${str}`;
118
114
  }
119
-
120
115
  function array() {
121
116
  const arr = [];
122
117
  return val => {
package/lib/index.js CHANGED
@@ -3,7 +3,6 @@
3
3
  const {
4
4
  start
5
5
  } = require('./viewer');
6
-
7
6
  module.exports = {
8
7
  start,
9
8
  BundleAnalyzerPlugin: require('./BundleAnalyzerPlugin')
package/lib/parseUtils.js CHANGED
@@ -1,21 +1,18 @@
1
1
  "use strict";
2
2
 
3
3
  const fs = require('fs');
4
-
5
- const _ = require('lodash');
6
-
7
4
  const acorn = require('acorn');
8
-
9
5
  const walk = require('acorn-walk');
10
-
11
6
  module.exports = {
12
7
  parseBundle
13
8
  };
14
-
15
- function parseBundle(bundlePath) {
9
+ function parseBundle(bundlePath, opts) {
10
+ const {
11
+ sourceType = 'script'
12
+ } = opts || {};
16
13
  const content = fs.readFileSync(bundlePath, 'utf8');
17
14
  const ast = acorn.parse(content, {
18
- sourceType: 'script',
15
+ sourceType,
19
16
  // I believe in a bright future of ECMAScript!
20
17
  // Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
21
18
  // Seems like `acorn` supports such weird option value.
@@ -29,22 +26,21 @@ function parseBundle(bundlePath) {
29
26
  ExpressionStatement(node, state, c) {
30
27
  if (state.locations) return;
31
28
  state.expressionStatementDepth++;
32
-
33
- if ( // Webpack 5 stores modules in the the top-level IIFE
29
+ if (
30
+ // Webpack 5 stores modules in the the top-level IIFE
34
31
  state.expressionStatementDepth === 1 && ast.body.includes(node) && isIIFE(node)) {
35
32
  const fn = getIIFECallExpression(node);
36
-
37
- if ( // It should not contain neither arguments
38
- fn.arguments.length === 0 && // ...nor parameters
33
+ if (
34
+ // It should not contain neither arguments
35
+ fn.arguments.length === 0 &&
36
+ // ...nor parameters
39
37
  fn.callee.params.length === 0) {
40
38
  // Modules are stored in the very first variable declaration as hash
41
39
  const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === 'VariableDeclaration');
42
-
43
40
  if (firstVariableDeclaration) {
44
41
  for (const declaration of firstVariableDeclaration.declarations) {
45
- if (declaration.init) {
42
+ if (declaration.init && isModulesList(declaration.init)) {
46
43
  state.locations = getModulesLocations(declaration.init);
47
-
48
44
  if (state.locations) {
49
45
  break;
50
46
  }
@@ -53,93 +49,85 @@ function parseBundle(bundlePath) {
53
49
  }
54
50
  }
55
51
  }
56
-
57
52
  if (!state.locations) {
58
53
  c(node.expression, state);
59
54
  }
60
-
61
55
  state.expressionStatementDepth--;
62
56
  },
63
-
64
57
  AssignmentExpression(node, state) {
65
- if (state.locations) return; // Modules are stored in exports.modules:
66
- // exports.modules = {};
58
+ if (state.locations) return;
67
59
 
60
+ // Modules are stored in exports.modules:
61
+ // exports.modules = {};
68
62
  const {
69
63
  left,
70
64
  right
71
65
  } = node;
72
-
73
66
  if (left && left.object && left.object.name === 'exports' && left.property && left.property.name === 'modules' && isModulesHash(right)) {
74
67
  state.locations = getModulesLocations(right);
75
68
  }
76
69
  },
77
-
78
70
  CallExpression(node, state, c) {
79
71
  if (state.locations) return;
80
- const args = node.arguments; // Main chunk with webpack loader.
72
+ const args = node.arguments;
73
+
74
+ // Main chunk with webpack loader.
81
75
  // Modules are stored in first argument:
82
76
  // (function (...) {...})(<modules>)
83
-
84
77
  if (node.callee.type === 'FunctionExpression' && !node.callee.id && args.length === 1 && isSimpleModulesList(args[0])) {
85
78
  state.locations = getModulesLocations(args[0]);
86
79
  return;
87
- } // Async Webpack < v4 chunk without webpack loader.
80
+ }
81
+
82
+ // Async Webpack < v4 chunk without webpack loader.
88
83
  // webpackJsonp([<chunks>], <modules>, ...)
89
84
  // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
90
-
91
-
92
85
  if (node.callee.type === 'Identifier' && mayBeAsyncChunkArguments(args) && isModulesList(args[1])) {
93
86
  state.locations = getModulesLocations(args[1]);
94
87
  return;
95
- } // Async Webpack v4 chunk without webpack loader.
88
+ }
89
+
90
+ // Async Webpack v4 chunk without webpack loader.
96
91
  // (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
97
92
  // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
98
-
99
-
100
93
  if (isAsyncChunkPushExpression(node)) {
101
94
  state.locations = getModulesLocations(args[0].elements[1]);
102
95
  return;
103
- } // Webpack v4 WebWorkerChunkTemplatePlugin
96
+ }
97
+
98
+ // Webpack v4 WebWorkerChunkTemplatePlugin
104
99
  // globalObject.chunkCallbackName([<chunks>],<modules>, ...);
105
100
  // Both globalObject and chunkCallbackName can be changed through the config, so we can't check them.
106
-
107
-
108
101
  if (isAsyncWebWorkerChunkExpression(node)) {
109
102
  state.locations = getModulesLocations(args[1]);
110
103
  return;
111
- } // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
112
- // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
113
-
104
+ }
114
105
 
106
+ // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
107
+ // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
115
108
  args.forEach(arg => c(arg, state));
116
109
  }
117
-
118
110
  });
119
- let modules;
120
-
111
+ const modules = {};
121
112
  if (walkState.locations) {
122
- modules = _.mapValues(walkState.locations, loc => content.slice(loc.start, loc.end));
123
- } else {
124
- modules = {};
113
+ Object.entries(walkState.locations).forEach(([id, loc]) => {
114
+ modules[id] = content.slice(loc.start, loc.end);
115
+ });
125
116
  }
126
-
127
117
  return {
128
118
  modules,
129
119
  src: content,
130
120
  runtimeSrc: getBundleRuntime(content, walkState.locations)
131
121
  };
132
122
  }
123
+
133
124
  /**
134
125
  * Returns bundle source except modules
135
126
  */
136
-
137
-
138
127
  function getBundleRuntime(content, modulesLocations) {
139
128
  const sortedLocations = Object.values(modulesLocations || {}).sort((a, b) => a.start - b.start);
140
129
  let result = '';
141
130
  let lastIndex = 0;
142
-
143
131
  for (const {
144
132
  start,
145
133
  end
@@ -147,14 +135,11 @@ function getBundleRuntime(content, modulesLocations) {
147
135
  result += content.slice(lastIndex, start);
148
136
  lastIndex = end;
149
137
  }
150
-
151
138
  return result + content.slice(lastIndex, content.length);
152
139
  }
153
-
154
140
  function isIIFE(node) {
155
141
  return node.type === 'ExpressionStatement' && (node.expression.type === 'CallExpression' || node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression');
156
142
  }
157
-
158
143
  function getIIFECallExpression(node) {
159
144
  if (node.expression.type === 'UnaryExpression') {
160
145
  return node.expression.argument;
@@ -162,59 +147,59 @@ function getIIFECallExpression(node) {
162
147
  return node.expression;
163
148
  }
164
149
  }
165
-
166
150
  function isModulesList(node) {
167
- return isSimpleModulesList(node) || // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
151
+ return isSimpleModulesList(node) ||
152
+ // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
168
153
  isOptimizedModulesArray(node);
169
154
  }
170
-
171
155
  function isSimpleModulesList(node) {
172
- return (// Modules are contained in hash. Keys are module ids.
173
- isModulesHash(node) || // Modules are contained in array. Indexes are module ids.
156
+ return (
157
+ // Modules are contained in hash. Keys are module ids.
158
+ isModulesHash(node) ||
159
+ // Modules are contained in array. Indexes are module ids.
174
160
  isModulesArray(node)
175
161
  );
176
162
  }
177
-
178
163
  function isModulesHash(node) {
179
164
  return node.type === 'ObjectExpression' && node.properties.map(node => node.value).every(isModuleWrapper);
180
165
  }
181
-
182
166
  function isModulesArray(node) {
183
- return node.type === 'ArrayExpression' && node.elements.every(elem => // Some of array items may be skipped because there is no module with such id
167
+ return node.type === 'ArrayExpression' && node.elements.every(elem =>
168
+ // Some of array items may be skipped because there is no module with such id
184
169
  !elem || isModuleWrapper(elem));
185
170
  }
186
-
187
171
  function isOptimizedModulesArray(node) {
188
172
  // Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
189
173
  // https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
190
174
  // The `<minimum ID>` + array indexes are module ids
191
- return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && // Make sure the object called is `Array(<some number>)`
192
- node.callee.object.type === 'CallExpression' && node.callee.object.callee.type === 'Identifier' && node.callee.object.callee.name === 'Array' && node.callee.object.arguments.length === 1 && isNumericId(node.callee.object.arguments[0]) && // Make sure the property X called for `Array(<some number>).X` is `concat`
193
- node.callee.property.type === 'Identifier' && node.callee.property.name === 'concat' && // Make sure exactly one array is passed in to `concat`
175
+ return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' &&
176
+ // Make sure the object called is `Array(<some number>)`
177
+ node.callee.object.type === 'CallExpression' && node.callee.object.callee.type === 'Identifier' && node.callee.object.callee.name === 'Array' && node.callee.object.arguments.length === 1 && isNumericId(node.callee.object.arguments[0]) &&
178
+ // Make sure the property X called for `Array(<some number>).X` is `concat`
179
+ node.callee.property.type === 'Identifier' && node.callee.property.name === 'concat' &&
180
+ // Make sure exactly one array is passed in to `concat`
194
181
  node.arguments.length === 1 && isModulesArray(node.arguments[0]);
195
182
  }
196
-
197
183
  function isModuleWrapper(node) {
198
- return (// It's an anonymous function expression that wraps module
199
- (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id || // If `DedupePlugin` is used it can be an ID of duplicated module...
200
- isModuleId(node) || // or an array of shape [<module_id>, ...args]
184
+ return (
185
+ // It's an anonymous function expression that wraps module
186
+ (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id ||
187
+ // If `DedupePlugin` is used it can be an ID of duplicated module...
188
+ isModuleId(node) ||
189
+ // or an array of shape [<module_id>, ...args]
201
190
  node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0])
202
191
  );
203
192
  }
204
-
205
193
  function isModuleId(node) {
206
194
  return node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string');
207
195
  }
208
-
209
196
  function isNumericId(node) {
210
197
  return node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0;
211
198
  }
212
-
213
199
  function isChunkIds(node) {
214
200
  // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
215
201
  return node.type === 'ArrayExpression' && node.elements.every(isModuleId);
216
202
  }
217
-
218
203
  function isAsyncChunkPushExpression(node) {
219
204
  const {
220
205
  callee,
@@ -222,11 +207,9 @@ function isAsyncChunkPushExpression(node) {
222
207
  } = node;
223
208
  return callee.type === 'MemberExpression' && callee.property.name === 'push' && callee.object.type === 'AssignmentExpression' && args.length === 1 && args[0].type === 'ArrayExpression' && mayBeAsyncChunkArguments(args[0].elements) && isModulesList(args[0].elements[1]);
224
209
  }
225
-
226
210
  function mayBeAsyncChunkArguments(args) {
227
211
  return args.length >= 2 && isChunkIds(args[0]);
228
212
  }
229
-
230
213
  function isAsyncWebWorkerChunkExpression(node) {
231
214
  const {
232
215
  callee,
@@ -235,7 +218,6 @@ function isAsyncWebWorkerChunkExpression(node) {
235
218
  } = node;
236
219
  return type === 'CallExpression' && callee.type === 'MemberExpression' && args.length === 2 && isChunkIds(args[0]) && isModulesList(args[1]);
237
220
  }
238
-
239
221
  function getModulesLocations(node) {
240
222
  if (node.type === 'ObjectExpression') {
241
223
  // Modules hash
@@ -246,28 +228,26 @@ function getModulesLocations(node) {
246
228
  return result;
247
229
  }, {});
248
230
  }
249
-
250
231
  const isOptimizedArray = node.type === 'CallExpression';
251
-
252
232
  if (node.type === 'ArrayExpression' || isOptimizedArray) {
253
233
  // Modules array or optimized array
254
- const minId = isOptimizedArray ? // Get the [minId] value from the Array() call first argument literal value
255
- node.callee.object.arguments[0].value : // `0` for simple array
234
+ const minId = isOptimizedArray ?
235
+ // Get the [minId] value from the Array() call first argument literal value
236
+ node.callee.object.arguments[0].value :
237
+ // `0` for simple array
256
238
  0;
257
- const modulesNodes = isOptimizedArray ? // The modules reside in the `concat()` function call arguments
239
+ const modulesNodes = isOptimizedArray ?
240
+ // The modules reside in the `concat()` function call arguments
258
241
  node.arguments[0].elements : node.elements;
259
242
  return modulesNodes.reduce((result, moduleNode, i) => {
260
243
  if (moduleNode) {
261
244
  result[i + minId] = getModuleLocation(moduleNode);
262
245
  }
263
-
264
246
  return result;
265
247
  }, {});
266
248
  }
267
-
268
249
  return {};
269
250
  }
270
-
271
251
  function getModuleLocation(node) {
272
252
  return {
273
253
  start: node.start,
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getCompressedSize = getCompressedSize;
7
+ exports.isZstdSupported = void 0;
8
+ const zlib = require('zlib');
9
+ const isZstdSupported = exports.isZstdSupported = 'createZstdCompress' in zlib;
10
+ function getCompressedSize(compressionAlgorithm, input) {
11
+ if (compressionAlgorithm === 'gzip') return zlib.gzipSync(input, {
12
+ level: 9
13
+ }).length;
14
+ if (compressionAlgorithm === 'brotli') return zlib.brotliCompressSync(input).length;
15
+ if (compressionAlgorithm === 'zstd' && isZstdSupported) {
16
+ return zlib.zstdCompressSync(input).length;
17
+ }
18
+ throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`);
19
+ }
package/lib/statsUtils.js CHANGED
@@ -3,31 +3,25 @@
3
3
  const {
4
4
  createWriteStream
5
5
  } = require('fs');
6
-
7
6
  const {
8
7
  Readable
9
8
  } = require('stream');
10
-
11
9
  class StatsSerializeStream extends Readable {
12
10
  constructor(stats) {
13
11
  super();
14
12
  this._indentLevel = 0;
15
13
  this._stringifier = this._stringify(stats);
16
14
  }
17
-
18
15
  get _indent() {
19
16
  return ' '.repeat(this._indentLevel);
20
17
  }
21
-
22
18
  _read() {
23
19
  let readMore = true;
24
-
25
20
  while (readMore) {
26
21
  const {
27
22
  value,
28
23
  done
29
24
  } = this._stringifier.next();
30
-
31
25
  if (done) {
32
26
  this.push(null);
33
27
  readMore = false;
@@ -36,7 +30,6 @@ class StatsSerializeStream extends Readable {
36
30
  }
37
31
  }
38
32
  }
39
-
40
33
  *_stringify(obj) {
41
34
  if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || obj === null) {
42
35
  yield JSON.stringify(obj);
@@ -44,17 +37,14 @@ class StatsSerializeStream extends Readable {
44
37
  yield '[';
45
38
  this._indentLevel++;
46
39
  let isFirst = true;
47
-
48
40
  for (let item of obj) {
49
41
  if (item === undefined) {
50
42
  item = null;
51
43
  }
52
-
53
44
  yield `${isFirst ? '' : ','}\n${this._indent}`;
54
45
  yield* this._stringify(item);
55
46
  isFirst = false;
56
47
  }
57
-
58
48
  this._indentLevel--;
59
49
  yield obj.length ? `\n${this._indent}]` : ']';
60
50
  } else {
@@ -62,27 +52,21 @@ class StatsSerializeStream extends Readable {
62
52
  this._indentLevel++;
63
53
  let isFirst = true;
64
54
  const entries = Object.entries(obj);
65
-
66
55
  for (const [itemKey, itemValue] of entries) {
67
56
  if (itemValue === undefined) {
68
57
  continue;
69
58
  }
70
-
71
59
  yield `${isFirst ? '' : ','}\n${this._indent}${JSON.stringify(itemKey)}: `;
72
60
  yield* this._stringify(itemValue);
73
61
  isFirst = false;
74
62
  }
75
-
76
63
  this._indentLevel--;
77
64
  yield entries.length ? `\n${this._indent}}` : '}';
78
65
  }
79
66
  }
80
-
81
67
  }
82
-
83
68
  exports.StatsSerializeStream = StatsSerializeStream;
84
69
  exports.writeStats = writeStats;
85
-
86
70
  async function writeStats(stats, filepath) {
87
71
  return new Promise((resolve, reject) => {
88
72
  new StatsSerializeStream(stats).on('end', resolve).on('error', reject).pipe(createWriteStream(filepath));
package/lib/template.js CHANGED
@@ -2,51 +2,45 @@
2
2
 
3
3
  /* eslint-disable max-len */
4
4
  const path = require('path');
5
-
6
5
  const fs = require('fs');
7
-
8
- const _ = require('lodash');
9
-
6
+ const {
7
+ escape
8
+ } = require('html-escaper');
10
9
  const projectRoot = path.resolve(__dirname, '..');
11
10
  const assetsRoot = path.join(projectRoot, 'public');
12
11
  exports.renderViewer = renderViewer;
12
+
13
13
  /**
14
14
  * Escapes `<` characters in JSON to safely use it in `<script>` tag.
15
15
  */
16
-
17
16
  function escapeJson(json) {
18
17
  return JSON.stringify(json).replace(/</gu, '\\u003c');
19
18
  }
20
-
21
19
  function getAssetContent(filename) {
22
20
  const assetPath = path.join(assetsRoot, filename);
23
-
24
21
  if (!assetPath.startsWith(assetsRoot)) {
25
22
  throw new Error(`"${filename}" is outside of the assets root`);
26
23
  }
27
-
28
24
  return fs.readFileSync(assetPath, 'utf8');
29
25
  }
30
-
31
26
  function html(strings, ...values) {
32
27
  return strings.map((string, index) => `${string}${values[index] || ''}`).join('');
33
28
  }
34
-
35
29
  function getScript(filename, mode) {
36
30
  if (mode === 'static') {
37
- return `<!-- ${_.escape(filename)} -->
31
+ return `<!-- ${escape(filename)} -->
38
32
  <script>${getAssetContent(filename)}</script>`;
39
33
  } else {
40
- return `<script src="${_.escape(filename)}"></script>`;
34
+ return `<script src="${escape(filename)}"></script>`;
41
35
  }
42
36
  }
43
-
44
37
  function renderViewer({
45
38
  title,
46
39
  enableWebSocket,
47
40
  chartData,
48
41
  entrypoints,
49
42
  defaultSizes,
43
+ compressionAlgorithm,
50
44
  mode
51
45
  } = {}) {
52
46
  return html`<!DOCTYPE html>
@@ -54,7 +48,7 @@ function renderViewer({
54
48
  <head>
55
49
  <meta charset="UTF-8"/>
56
50
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
57
- <title>${_.escape(title)}</title>
51
+ <title>${escape(title)}</title>
58
52
  <link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABrVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+O1foceMD///+J0/qK1Pr7/v8Xdr/9///W8P4UdL7L7P0Scr2r4Pyj3vwad8D5/f/2/f+55f3E6f34+/2H0/ojfMKpzOd0rNgQcb3F3O/j9f7c8v6g3Pz0/P/w+v/q+P7n9v6T1/uQ1vuE0vqLut/y+v+Z2fvt+f+15Pzv9fuc2/vR7v2V2Pvd6/bg9P7I6/285/2y4/yp3/zp8vk8i8kqgMT7/P31+fyv4vxGkcz6/P6/6P3j7vfS5PNnpNUxhcbO7f7F6v3O4vHK3/DA2u631Ouy0eqXweKJud5wqthfoNMMbLvY8f73+v2dxeR8sNtTmdDx9/zX6PSjyeaCtd1YnNGX2PuQveCGt95Nls42h8dLlM3F4vBtAAAAM3RSTlMAAyOx0/sKBvik8opWGBMOAe3l1snDm2E9LSb06eHcu5JpHbarfHZCN9CBb08zzkdNS0kYaptYAAAFV0lEQVRYw92X51/aYBDHHS2O2qqttVbrqNq9m+TJIAYIShBkWwqIiCgoWvfeq7Z2/s29hyQNyUcR7LveGwVyXy6XH8/9rqxglLfUPLxVduUor3h0rfp2TYvpivk37929TkG037hffoX0+peVtZQc1589rigVUdXS/ABSAyEmGIO/1XfvldSK8vs3OqB6u3m0nxmIrvgB0dj7rr7Y9IbuF68hnfFaiHA/sxqm0wciIG43P60qKv9WXWc1RXGh/mFESFABTSBi0sNAKzqet17eCtOb3kZIDwxEEU0oAIJGYxNBDhBND29e0rtXXbcpuPmED9IhEAAQ/AXEaF8EPmnrrKsv0LvWR3fg5sWDNAFZOgAgaKvZDogHNU9MFwnnYROkc56RD5CjAbQX9Ow4g7upCsvYu55aSI/Nj0H1akgKQEUM94dwK65hYRmFU9MIcH/fqJYOZYcnuJSU/waKDgTOEVaVKhwrTRP5XzgSpAITYzom7UvkhFX5VutmxeNnWDjjswTKTyfgluNDGbUpWissXhF3s7mlSml+czWkg3D0l1nNjGNjz3myOQOa1KM/jOS6ebdbAVTCi4gljHSFrviza7tOgRWcS0MOUX9zdNgag5w7rRqA44Lzw0hr1WqES36dFliSJFlh2rXIae3FFcDDgKdxrUIDePr8jGcSClV1u7A9xeN0ModY/pHMxmR1EzRh8TJiwqsHmKW0l4FCEZI+jHio+JdPPE9qwQtTRxku2D8sIeRL2LnxWSllANCQGOIiqVHAz2ye2JR0DcH+HoxDkaADLjgxjKQ+AwCX/g0+DNgdG0ukYCONAe+dbc2IAc6fwt1ARoDSezNHxV2Cmzwv3O6lDMV55edBGwGK9n1+x2F8EDfAGCxug8MhpsMEcTEAWf3rx2vZhe/LAmtIn/6apE6PN0ULKgywD9mmdxbmFl3OvD5AS5fW5zLbv/YHmcsBTjf/afDz3MaZTVCfAP9z6/Bw6ycv8EUBWJIn9zYcoAWWlW9+OzO3vkTy8H+RANLmdrpOuYWdZYEXpo+TlCJrW5EARb7fF+bWdqf3hhyZI1nWJQHgznErZhbjoEsWqi8dQNoE294aldzFurwSABL2XXMf9+H1VQGke9exw5P/AnA5Pv5ngMul7LOvO922iwACu8WkCwLCafvM4CeWPxfA8lNHcWZSoi8EwMAIciKX2Z4SWCMAa3snCZ/G4EA8D6CMLNFsGQhkkz/gQNEBbPCbWsxGUpYVu3z8IyNAknwJkfPMEhLyrdi5RTyUVACkw4GSFRNWJNEW+fgPGwHD8/JxnRuLabN4CGNRkAE23na2+VmEAUmrYymSGjMAYqH84YUIyzgzs3XC7gNgH36Vcc4zKY9o9fgPBXUAiHHwVboBHGLiX6Zcjp1f2wu4tvzZKo0ecPnDtQYDQvJXaBeNzce45Fp28ZQLrEZVuFqgBwOalArKXnW1UzlnSusQKJqKYNuz4tOnI6sZG4zanpemv+7ySU2jbA9h6uhcgpfy6G2PahirDZ6zvq6zDduMVFTKvzw8wgyEdelwY9in3XkEPs3osJuwRQ4qTkfzifndg9Gfc4pdsu82+tTnHZTBa2EAMrqr2t43pguc8tNm7JQVQ2S0ukj2d22dhXYP0/veWtwKrCkNoNimAN5+Xr/oLrxswKbVJjteWrX7eR63o4j9q0GxnaBdWgGA5VStpanIjQmEhV0/nVt5VOFUvix6awJhPcAaTEShgrG+iGyvb5a0Ndb1YGHFPEwoqAinoaykaID1o1pdPNu7XsnCKQ3R+hwWIIhGvORcJUBYXe3Xa3vq/mF/N9V13ugufMkfXn+KHsRD0B8AAAAASUVORK5CYII=" type="image/x-icon" />
59
53
 
60
54
  <script>
@@ -69,6 +63,7 @@ function renderViewer({
69
63
  window.chartData = ${escapeJson(chartData)};
70
64
  window.entrypoints = ${escapeJson(entrypoints)};
71
65
  window.defaultSizes = ${escapeJson(defaultSizes)};
66
+ window.compressionAlgorithm = ${escapeJson(compressionAlgorithm)};
72
67
  </script>
73
68
  </body>
74
69
  </html>`;