webpack-bundle-analyzer 4.0.0 → 4.4.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.
@@ -1,11 +1,11 @@
1
- const bfj = require('bfj');
1
+ const fs = require('fs');
2
2
  const path = require('path');
3
- const mkdir = require('mkdirp');
4
3
  const {bold} = require('chalk');
5
4
 
6
5
  const Logger = require('./Logger');
7
6
  const viewer = require('./viewer');
8
7
  const utils = require('./utils');
8
+ const {writeStats} = require('./statsUtils');
9
9
 
10
10
  class BundleAnalyzerPlugin {
11
11
  constructor(opts = {}) {
@@ -80,17 +80,10 @@ class BundleAnalyzerPlugin {
80
80
 
81
81
  async generateStatsFile(stats) {
82
82
  const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename);
83
- mkdir.sync(path.dirname(statsFilepath));
83
+ await fs.promises.mkdir(path.dirname(statsFilepath), {recursive: true});
84
84
 
85
85
  try {
86
- await bfj.write(statsFilepath, stats, {
87
- space: 2,
88
- promises: 'ignore',
89
- buffers: 'ignore',
90
- maps: 'ignore',
91
- iterables: 'ignore',
92
- circular: 'ignore'
93
- });
86
+ await writeStats(stats, statsFilepath);
94
87
 
95
88
  this.logger.info(
96
89
  `${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`
package/src/analyzer.js CHANGED
@@ -32,7 +32,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
32
32
  // Sometimes if there are additional child chunks produced add them as child assets,
33
33
  // leave the 1st one as that is considered the 'root' asset.
34
34
  for (let i = 1; i < children.length; i++) {
35
- bundleStats.children[i].assets.forEach((asset) => {
35
+ children[i].assets.forEach((asset) => {
36
36
  asset.isChild = true;
37
37
  bundleStats.assets.push(asset);
38
38
  });
@@ -48,7 +48,12 @@ function getViewerData(bundleStats, bundleDir, opts) {
48
48
  }
49
49
 
50
50
  // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
51
- bundleStats.assets = _.filter(bundleStats.assets, asset => {
51
+ bundleStats.assets = bundleStats.assets.filter(asset => {
52
+ // Filter out non 'asset' type asset if type is provided (Webpack 5 add a type to indicate asset types)
53
+ if (asset.type && asset.type !== 'asset') {
54
+ return false;
55
+ }
56
+
52
57
  // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
53
58
  // See #22
54
59
  asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
@@ -77,7 +82,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
77
82
  }
78
83
 
79
84
  bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
80
- _.assign(parsedModules, bundleInfo.modules);
85
+ Object.assign(parsedModules, bundleInfo.modules);
81
86
  }
82
87
 
83
88
  if (_.isEmpty(bundlesSources)) {
@@ -87,7 +92,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
87
92
  }
88
93
  }
89
94
 
90
- const assets = _.transform(bundleStats.assets, (result, statAsset) => {
95
+ const assets = bundleStats.assets.reduce((result, statAsset) => {
91
96
  // If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
92
97
  const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
93
98
  const modules = assetBundles ? getBundleModules(assetBundles) : [];
@@ -139,22 +144,21 @@ function getViewerData(bundleStats, bundleDir, opts) {
139
144
 
140
145
  asset.modules = assetModules;
141
146
  asset.tree = createModulesTree(asset.modules);
147
+ return result;
142
148
  }, {});
143
149
 
144
- return _.transform(assets, (result, asset, filename) => {
145
- result.push({
146
- label: filename,
147
- isAsset: true,
148
- // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
149
- // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
150
- // be the size of minified bundle.
151
- // Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
152
- statSize: asset.tree.size || asset.size,
153
- parsedSize: asset.parsedSize,
154
- gzipSize: asset.gzipSize,
155
- groups: _.invokeMap(asset.tree.children, 'toChartData')
156
- });
157
- }, []);
150
+ return Object.entries(assets).map(([filename, asset]) => ({
151
+ label: filename,
152
+ isAsset: true,
153
+ // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
154
+ // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
155
+ // be the size of minified bundle.
156
+ // Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
157
+ statSize: asset.tree.size || asset.size,
158
+ parsedSize: asset.parsedSize,
159
+ gzipSize: asset.gzipSize,
160
+ groups: _.invokeMap(asset.tree.children, 'toChartData')
161
+ }));
158
162
  }
159
163
 
160
164
  function readStatsFromFile(filename) {
@@ -164,7 +168,7 @@ function readStatsFromFile(filename) {
164
168
  }
165
169
 
166
170
  function getChildAssetBundles(bundleStats, assetName) {
167
- return _.find(bundleStats.children, (c) =>
171
+ return (bundleStats.children || []).find((c) =>
168
172
  _(c.assetsByChunkName)
169
173
  .values()
170
174
  .flatten()
@@ -186,8 +190,8 @@ function getBundleModules(bundleStats) {
186
190
 
187
191
  function assetHasModule(statAsset, statModule) {
188
192
  // Checking if this module is the part of asset chunks
189
- return _.some(statModule.chunks, moduleChunk =>
190
- _.includes(statAsset.chunks, moduleChunk)
193
+ return statModule.chunks.some(moduleChunk =>
194
+ statAsset.chunks.includes(moduleChunk)
191
195
  );
192
196
  }
193
197
 
@@ -202,7 +206,7 @@ function isRuntimeModule(statModule) {
202
206
  function createModulesTree(modules) {
203
207
  const root = new Folder('.');
204
208
 
205
- _.each(modules, module => root.addModule(module));
209
+ modules.forEach(module => root.addModule(module));
206
210
  root.mergeNestedFolders();
207
211
 
208
212
  return root;
@@ -2,7 +2,6 @@
2
2
 
3
3
  const {resolve, dirname} = require('path');
4
4
 
5
- const _ = require('lodash');
6
5
  const commander = require('commander');
7
6
  const {magenta} = require('chalk');
8
7
 
@@ -157,7 +156,7 @@ function showHelp(error) {
157
156
  }
158
157
 
159
158
  function br(str) {
160
- return `\n${_.repeat(' ', 28)}${str}`;
159
+ return `\n${' '.repeat(28)}${str}`;
161
160
  }
162
161
 
163
162
  function array() {
package/src/parseUtils.js CHANGED
@@ -135,7 +135,7 @@ function parseBundle(bundlePath) {
135
135
 
136
136
  // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
137
137
  // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
138
- _.each(args, arg => c(arg, state));
138
+ args.forEach(arg => c(arg, state));
139
139
  }
140
140
  }
141
141
  );
@@ -161,9 +161,8 @@ function parseBundle(bundlePath) {
161
161
  * Returns bundle source except modules
162
162
  */
163
163
  function getBundleRuntime(content, modulesLocations) {
164
- const sortedLocations = _(modulesLocations)
165
- .values()
166
- .sortBy('start');
164
+ const sortedLocations = Object.values(modulesLocations || {})
165
+ .sort((a, b) => a.start - b.start);
167
166
 
168
167
  let result = '';
169
168
  let lastIndex = 0;
@@ -214,8 +213,8 @@ function isSimpleModulesList(node) {
214
213
  function isModulesHash(node) {
215
214
  return (
216
215
  node.type === 'ObjectExpression' &&
217
- _(node.properties)
218
- .map('value')
216
+ node.properties
217
+ .map(node => node.value)
219
218
  .every(isModuleWrapper)
220
219
  );
221
220
  }
@@ -223,7 +222,7 @@ function isModulesHash(node) {
223
222
  function isModulesArray(node) {
224
223
  return (
225
224
  node.type === 'ArrayExpression' &&
226
- _.every(node.elements, elem =>
225
+ node.elements.every(elem =>
227
226
  // Some of array items may be skipped because there is no module with such id
228
227
  !elem ||
229
228
  isModuleWrapper(elem)
@@ -276,7 +275,7 @@ function isChunkIds(node) {
276
275
  // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
277
276
  return (
278
277
  node.type === 'ArrayExpression' &&
279
- _.every(node.elements, isModuleId)
278
+ node.elements.every(isModuleId)
280
279
  );
281
280
  }
282
281
 
@@ -321,10 +320,11 @@ function getModulesLocations(node) {
321
320
  // Modules hash
322
321
  const modulesNodes = node.properties;
323
322
 
324
- return _.transform(modulesNodes, (result, moduleNode) => {
323
+ return modulesNodes.reduce((result, moduleNode) => {
325
324
  const moduleId = moduleNode.key.name || moduleNode.key.value;
326
325
 
327
326
  result[moduleId] = getModuleLocation(moduleNode.value);
327
+ return result;
328
328
  }, {});
329
329
  }
330
330
 
@@ -342,9 +342,11 @@ function getModulesLocations(node) {
342
342
  node.arguments[0].elements :
343
343
  node.elements;
344
344
 
345
- return _.transform(modulesNodes, (result, moduleNode, i) => {
346
- if (!moduleNode) return;
347
- result[i + minId] = getModuleLocation(moduleNode);
345
+ return modulesNodes.reduce((result, moduleNode, i) => {
346
+ if (moduleNode) {
347
+ result[i + minId] = getModuleLocation(moduleNode);
348
+ }
349
+ return result;
348
350
  }, {});
349
351
  }
350
352
 
@@ -0,0 +1,82 @@
1
+ const {createWriteStream} = require('fs');
2
+ const {Readable} = require('stream');
3
+
4
+ class StatsSerializeStream extends Readable {
5
+ constructor(stats) {
6
+ super();
7
+ this._indentLevel = 0;
8
+ this._stringifier = this._stringify(stats);
9
+ }
10
+
11
+ get _indent() {
12
+ return ' '.repeat(this._indentLevel);
13
+ }
14
+
15
+ _read() {
16
+ let readMore = true;
17
+
18
+ while (readMore) {
19
+ const {value, done} = this._stringifier.next();
20
+
21
+ if (done) {
22
+ this.push(null);
23
+ readMore = false;
24
+ } else {
25
+ readMore = this.push(value);
26
+ }
27
+ }
28
+ }
29
+
30
+ * _stringify(obj) {
31
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || obj === null) {
32
+ yield JSON.stringify(obj);
33
+ } else if (Array.isArray(obj)) {
34
+ yield '[';
35
+ this._indentLevel++;
36
+
37
+ let isFirst = true;
38
+ for (let item of obj) {
39
+ if (item === undefined) {
40
+ item = null;
41
+ }
42
+
43
+ yield `${isFirst ? '' : ','}\n${this._indent}`;
44
+ yield* this._stringify(item);
45
+ isFirst = false;
46
+ }
47
+
48
+ this._indentLevel--;
49
+ yield obj.length ? `\n${this._indent}]` : ']';
50
+ } else {
51
+ yield '{';
52
+ this._indentLevel++;
53
+
54
+ let isFirst = true;
55
+ const entries = Object.entries(obj);
56
+ for (const [itemKey, itemValue] of entries) {
57
+ if (itemValue === undefined) {
58
+ continue;
59
+ }
60
+
61
+ yield `${isFirst ? '' : ','}\n${this._indent}${JSON.stringify(itemKey)}: `;
62
+ yield* this._stringify(itemValue);
63
+ isFirst = false;
64
+ }
65
+
66
+ this._indentLevel--;
67
+ yield entries.length ? `\n${this._indent}}` : '}';
68
+ }
69
+ }
70
+ }
71
+
72
+ exports.StatsSerializeStream = StatsSerializeStream;
73
+ exports.writeStats = writeStats;
74
+
75
+ async function writeStats(stats, filepath) {
76
+ return new Promise((resolve, reject) => {
77
+ new StatsSerializeStream(stats)
78
+ .on('end', resolve)
79
+ .on('error', reject)
80
+ .pipe(createWriteStream(filepath));
81
+ });
82
+ }
@@ -1,22 +1,65 @@
1
- <!DOCTYPE html>
1
+ /* eslint-disable max-len */
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const _ = require('lodash');
6
+
7
+ const projectRoot = path.resolve(__dirname, '..');
8
+ const assetsRoot = path.join(projectRoot, 'public');
9
+
10
+ exports.renderViewer = renderViewer;
11
+
12
+ /**
13
+ * Escapes `<` characters in JSON to safely use it in `<script>` tag.
14
+ */
15
+ function escapeJson(json) {
16
+ return JSON.stringify(json).replace(/</gu, '\\u003c');
17
+ }
18
+
19
+ function getAssetContent(filename) {
20
+ const assetPath = path.join(assetsRoot, filename);
21
+
22
+ if (!assetPath.startsWith(assetsRoot)) {
23
+ throw new Error(`"${filename}" is outside of the assets root`);
24
+ }
25
+
26
+ return fs.readFileSync(assetPath, 'utf8');
27
+ }
28
+
29
+ function html(strings, ...values) {
30
+ return strings.map((string, index) => `${string}${values[index] || ''}`).join('');
31
+ }
32
+
33
+ function getScript(filename, mode) {
34
+ if (mode === 'static') {
35
+ return `<!-- ${_.escape(filename)} -->
36
+ <script>${getAssetContent(filename)}</script>`;
37
+ } else {
38
+ return `<script src="${_.escape(filename)}"></script>`;
39
+ }
40
+ }
41
+
42
+ function renderViewer({title, enableWebSocket, chartData, defaultSizes, mode} = {}) {
43
+ return html`<!DOCTYPE html>
2
44
  <html>
3
45
  <head>
4
46
  <meta charset="UTF-8"/>
5
47
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
- <title><%= title %></title>
48
+ <title>${_.escape(title)}</title>
7
49
  <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" />
8
50
 
9
51
  <script>
10
- window.enableWebSocket = <%- escapeJson(enableWebSocket) %>;
52
+ window.enableWebSocket = ${escapeJson(enableWebSocket)};
11
53
  </script>
12
- <%- include('script', { filename: 'viewer.js' }) %>
54
+ ${getScript('viewer.js', mode)}
13
55
  </head>
14
56
 
15
57
  <body>
16
58
  <div id="app"></div>
17
59
  <script>
18
- window.chartData = <%- escapeJson(chartData) %>;
19
- window.defaultSizes = <%- escapeJson(defaultSizes) %>;
60
+ window.chartData = ${escapeJson(chartData)};
61
+ window.defaultSizes = ${escapeJson(defaultSizes)};
20
62
  </script>
21
63
  </body>
22
- </html>
64
+ </html>`;
65
+ }
@@ -62,7 +62,7 @@ export default class BaseFolder extends Node {
62
62
  walk(walker, state = {}, deep = true) {
63
63
  let stopped = false;
64
64
 
65
- _.each(this.children, child => {
65
+ Object.values(this.children).forEach(child => {
66
66
  if (deep && child.walk) {
67
67
  state = child.walk(walker, state, stop);
68
68
  } else {
@@ -15,7 +15,7 @@ export default class ConcatenatedModule extends Module {
15
15
  }
16
16
 
17
17
  fillContentModules() {
18
- _.each(this.data.modules, moduleData => this.addContentModule(moduleData));
18
+ this.data.modules.forEach(moduleData => this.addContentModule(moduleData));
19
19
  }
20
20
 
21
21
  addContentModule(moduleData) {
@@ -28,7 +28,7 @@ export default class ConcatenatedModule extends Module {
28
28
  const [folders, fileName] = [pathParts.slice(0, -1), _.last(pathParts)];
29
29
  let currentFolder = this;
30
30
 
31
- _.each(folders, folderName => {
31
+ folders.forEach(folderName => {
32
32
  let childFolder = currentFolder.getChild(folderName);
33
33
 
34
34
  if (!childFolder) {
@@ -30,7 +30,7 @@ export default class Folder extends BaseFolder {
30
30
  const [folders, fileName] = [pathParts.slice(0, -1), _.last(pathParts)];
31
31
  let currentFolder = this;
32
32
 
33
- _.each(folders, folderName => {
33
+ folders.forEach(folderName => {
34
34
  let childNode = currentFolder.getChild(folderName);
35
35
 
36
36
  if (
package/src/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- const {inspect} = require('util');
1
+ const {inspect, types} = require('util');
2
2
  const _ = require('lodash');
3
3
  const opener = require('opener');
4
4
 
@@ -15,11 +15,11 @@ function createAssetsFilter(excludePatterns) {
15
15
  pattern = new RegExp(pattern, 'u');
16
16
  }
17
17
 
18
- if (_.isRegExp(pattern)) {
18
+ if (types.isRegExp(pattern)) {
19
19
  return (asset) => pattern.test(asset);
20
20
  }
21
21
 
22
- if (!_.isFunction(pattern)) {
22
+ if (typeof pattern !== 'function') {
23
23
  throw new TypeError(
24
24
  `Pattern should be either string, RegExp or a function, but "${inspect(pattern, {depth: 0})}" got.`
25
25
  );
@@ -30,7 +30,7 @@ function createAssetsFilter(excludePatterns) {
30
30
  .value();
31
31
 
32
32
  if (excludeFunctions.length) {
33
- return (asset) => _.every(excludeFunctions, fn => fn(asset) !== true);
33
+ return (asset) => excludeFunctions.every(fn => fn(asset) !== true);
34
34
  } else {
35
35
  return () => true;
36
36
  }
package/src/viewer.js CHANGED
@@ -3,18 +3,16 @@ const fs = require('fs');
3
3
  const http = require('http');
4
4
 
5
5
  const WebSocket = require('ws');
6
+ const sirv = require('sirv');
6
7
  const _ = require('lodash');
7
- const express = require('express');
8
- const ejs = require('ejs');
9
- const mkdir = require('mkdirp');
10
8
  const {bold} = require('chalk');
11
9
 
12
10
  const Logger = require('./Logger');
13
11
  const analyzer = require('./analyzer');
14
12
  const {open} = require('./utils');
13
+ const {renderViewer} = require('./template');
15
14
 
16
15
  const projectRoot = path.resolve(__dirname, '..');
17
- const assetsRoot = path.join(projectRoot, 'public');
18
16
 
19
17
  function resolveTitle(reportTitle) {
20
18
  if (typeof reportTitle === 'function') {
@@ -50,28 +48,26 @@ async function startServer(bundleStats, opts) {
50
48
 
51
49
  if (!chartData) return;
52
50
 
53
- const app = express();
54
-
55
- // Explicitly using our `ejs` dependency to render templates
56
- // Fixes #17
57
- app.engine('ejs', require('ejs').renderFile);
58
- app.set('view engine', 'ejs');
59
- app.set('views', `${projectRoot}/views`);
60
- app.use(express.static(`${projectRoot}/public`));
61
-
62
- app.use('/', (req, res) => {
63
- res.render('viewer', {
64
- mode: 'server',
65
- title: resolveTitle(reportTitle),
66
- get chartData() { return chartData },
67
- defaultSizes,
68
- enableWebSocket: true,
69
- // Helpers
70
- escapeJson
71
- });
51
+ const sirvMiddleware = sirv(`${projectRoot}/public`, {
52
+ // disables caching and traverse the file system on every request
53
+ dev: true
72
54
  });
73
55
 
74
- const server = http.createServer(app);
56
+ const server = http.createServer((req, res) => {
57
+ if (req.method === 'GET' && req.url === '/') {
58
+ const html = renderViewer({
59
+ mode: 'server',
60
+ title: resolveTitle(reportTitle),
61
+ chartData,
62
+ defaultSizes,
63
+ enableWebSocket: true
64
+ });
65
+ res.writeHead(200, {'Content-Type': 'text/html'});
66
+ res.end(html);
67
+ } else {
68
+ sirvMiddleware(req, res);
69
+ }
70
+ });
75
71
 
76
72
  await new Promise(resolve => {
77
73
  server.listen(port, host, () => {
@@ -140,44 +136,23 @@ async function generateReport(bundleStats, opts) {
140
136
 
141
137
  if (!chartData) return;
142
138
 
143
- await new Promise((resolve, reject) => {
144
- ejs.renderFile(
145
- `${projectRoot}/views/viewer.ejs`,
146
- {
147
- mode: 'static',
148
- title: resolveTitle(reportTitle),
149
- chartData,
150
- defaultSizes,
151
- enableWebSocket: false,
152
- // Helpers
153
- assetContent: getAssetContent,
154
- escapeJson
155
- },
156
- (err, reportHtml) => {
157
- try {
158
- if (err) {
159
- logger.error(err);
160
- reject(err);
161
- return;
162
- }
163
-
164
- const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
165
-
166
- mkdir.sync(path.dirname(reportFilepath));
167
- fs.writeFileSync(reportFilepath, reportHtml);
168
-
169
- logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
170
-
171
- if (openBrowser) {
172
- open(`file://${reportFilepath}`, logger);
173
- }
174
- resolve();
175
- } catch (e) {
176
- reject(e);
177
- }
178
- }
179
- );
139
+ const reportHtml = renderViewer({
140
+ mode: 'static',
141
+ title: resolveTitle(reportTitle),
142
+ chartData,
143
+ defaultSizes,
144
+ enableWebSocket: false
180
145
  });
146
+ const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
147
+
148
+ fs.mkdirSync(path.dirname(reportFilepath), {recursive: true});
149
+ fs.writeFileSync(reportFilepath, reportHtml);
150
+
151
+ logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
152
+
153
+ if (openBrowser) {
154
+ open(`file://${reportFilepath}`, logger);
155
+ }
181
156
  }
182
157
 
183
158
  async function generateJSONReport(bundleStats, opts) {
@@ -187,29 +162,12 @@ async function generateJSONReport(bundleStats, opts) {
187
162
 
188
163
  if (!chartData) return;
189
164
 
190
- mkdir.sync(path.dirname(reportFilename));
191
- fs.writeFileSync(reportFilename, JSON.stringify(chartData));
165
+ await fs.promises.mkdir(path.dirname(reportFilename), {recursive: true});
166
+ await fs.promises.writeFile(reportFilename, JSON.stringify(chartData));
192
167
 
193
168
  logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`);
194
169
  }
195
170
 
196
- function getAssetContent(filename) {
197
- const assetPath = path.join(assetsRoot, filename);
198
-
199
- if (!assetPath.startsWith(assetsRoot)) {
200
- throw new Error(`"${filename}" is outside of the assets root`);
201
- }
202
-
203
- return fs.readFileSync(assetPath, 'utf8');
204
- }
205
-
206
- /**
207
- * Escapes `<` characters in JSON to safely use it in `<script>` tag.
208
- */
209
- function escapeJson(json) {
210
- return JSON.stringify(json).replace(/</gu, '\\u003c');
211
- }
212
-
213
171
  function getChartData(analyzerOpts, ...args) {
214
172
  let chartData;
215
173
  const {logger} = analyzerOpts;
package/views/script.ejs DELETED
@@ -1,8 +0,0 @@
1
- <% if (mode === 'static') { %>
2
- <!-- <%= filename %> -->
3
- <script>
4
- <%- assetContent(filename) %>
5
- </script>
6
- <% } else { %>
7
- <script src="<%= filename %>"></script>
8
- <% } %>