webpack-bundle-analyzer 4.1.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,15 @@ _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
+
17
26
  ## 4.1.0
18
27
 
19
28
  * **Improvement**
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
- const path = require('path');
3
+ const fs = require('fs');
4
4
 
5
- const mkdir = require('mkdirp');
5
+ const path = require('path');
6
6
 
7
7
  const {
8
8
  bold
@@ -90,7 +90,9 @@ class BundleAnalyzerPlugin {
90
90
 
91
91
  async generateStatsFile(stats) {
92
92
  const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename);
93
- mkdir.sync(path.dirname(statsFilepath));
93
+ await fs.promises.mkdir(path.dirname(statsFilepath), {
94
+ recursive: true
95
+ });
94
96
 
95
97
  try {
96
98
  await writeStats(stats, statsFilepath);
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
@@ -85,8 +90,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
85
90
  }
86
91
 
87
92
  bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
88
-
89
- _.assign(parsedModules, bundleInfo.modules);
93
+ Object.assign(parsedModules, bundleInfo.modules);
90
94
  }
91
95
 
92
96
  if (_.isEmpty(bundlesSources)) {
@@ -96,7 +100,7 @@ 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) : [];
@@ -149,22 +153,20 @@ function getViewerData(bundleStats, bundleDir, opts) {
149
153
 
150
154
  asset.modules = assetModules;
151
155
  asset.tree = createModulesTree(asset.modules);
156
+ return result;
152
157
  }, {});
153
-
154
- return _.transform(assets, (result, asset, filename) => {
155
- result.push({
156
- label: filename,
157
- isAsset: true,
158
- // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
159
- // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
160
- // be the size of minified bundle.
161
- // Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
162
- statSize: asset.tree.size || asset.size,
163
- parsedSize: asset.parsedSize,
164
- gzipSize: asset.gzipSize,
165
- groups: _.invokeMap(asset.tree.children, 'toChartData')
166
- });
167
- }, []);
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
+ }));
168
170
  }
169
171
 
170
172
  function readStatsFromFile(filename) {
@@ -172,7 +174,7 @@ function readStatsFromFile(filename) {
172
174
  }
173
175
 
174
176
  function getChildAssetBundles(bundleStats, assetName) {
175
- return _.find(bundleStats.children, c => _(c.assetsByChunkName).values().flatten().includes(assetName));
177
+ return (bundleStats.children || []).find(c => _(c.assetsByChunkName).values().flatten().includes(assetName));
176
178
  }
177
179
 
178
180
  function getBundleModules(bundleStats) {
@@ -182,7 +184,7 @@ function getBundleModules(bundleStats) {
182
184
 
183
185
  function assetHasModule(statAsset, statModule) {
184
186
  // Checking if this module is the part of asset chunks
185
- return _.some(statModule.chunks, moduleChunk => _.includes(statAsset.chunks, moduleChunk));
187
+ return statModule.chunks.some(moduleChunk => statAsset.chunks.includes(moduleChunk));
186
188
  }
187
189
 
188
190
  function isEntryModule(statModule) {
@@ -195,9 +197,7 @@ function isRuntimeModule(statModule) {
195
197
 
196
198
  function createModulesTree(modules) {
197
199
  const root = new Folder('.');
198
-
199
- _.each(modules, module => root.addModule(module));
200
-
200
+ modules.forEach(module => root.addModule(module));
201
201
  root.mergeNestedFolders();
202
202
  return root;
203
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
@@ -112,7 +112,7 @@ function parseBundle(bundlePath) {
112
112
  // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
113
113
 
114
114
 
115
- _.each(args, arg => c(arg, state));
115
+ args.forEach(arg => c(arg, state));
116
116
  }
117
117
 
118
118
  });
@@ -136,8 +136,7 @@ function parseBundle(bundlePath) {
136
136
 
137
137
 
138
138
  function getBundleRuntime(content, modulesLocations) {
139
- const sortedLocations = _(modulesLocations).values().sortBy('start');
140
-
139
+ const sortedLocations = Object.values(modulesLocations || {}).sort((a, b) => a.start - b.start);
141
140
  let result = '';
142
141
  let lastIndex = 0;
143
142
 
@@ -177,11 +176,11 @@ function isSimpleModulesList(node) {
177
176
  }
178
177
 
179
178
  function isModulesHash(node) {
180
- return node.type === 'ObjectExpression' && _(node.properties).map('value').every(isModuleWrapper);
179
+ return node.type === 'ObjectExpression' && node.properties.map(node => node.value).every(isModuleWrapper);
181
180
  }
182
181
 
183
182
  function isModulesArray(node) {
184
- 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
185
184
  !elem || isModuleWrapper(elem));
186
185
  }
187
186
 
@@ -213,7 +212,7 @@ function isNumericId(node) {
213
212
 
214
213
  function isChunkIds(node) {
215
214
  // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
216
- return node.type === 'ArrayExpression' && _.every(node.elements, isModuleId);
215
+ return node.type === 'ArrayExpression' && node.elements.every(isModuleId);
217
216
  }
218
217
 
219
218
  function isAsyncChunkPushExpression(node) {
@@ -241,9 +240,10 @@ function getModulesLocations(node) {
241
240
  if (node.type === 'ObjectExpression') {
242
241
  // Modules hash
243
242
  const modulesNodes = node.properties;
244
- return _.transform(modulesNodes, (result, moduleNode) => {
243
+ return modulesNodes.reduce((result, moduleNode) => {
245
244
  const moduleId = moduleNode.key.name || moduleNode.key.value;
246
245
  result[moduleId] = getModuleLocation(moduleNode.value);
246
+ return result;
247
247
  }, {});
248
248
  }
249
249
 
@@ -256,9 +256,12 @@ function getModulesLocations(node) {
256
256
  0;
257
257
  const modulesNodes = isOptimizedArray ? // The modules reside in the `concat()` function call arguments
258
258
  node.arguments[0].elements : node.elements;
259
- return _.transform(modulesNodes, (result, moduleNode, i) => {
260
- if (!moduleNode) return;
261
- 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;
262
265
  }, {});
263
266
  }
264
267
 
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ /* eslint-disable max-len */
4
+ const path = require('path');
5
+
6
+ const fs = require('fs');
7
+
8
+ const _ = require('lodash');
9
+
10
+ const projectRoot = path.resolve(__dirname, '..');
11
+ const assetsRoot = path.join(projectRoot, 'public');
12
+ exports.renderViewer = renderViewer;
13
+ /**
14
+ * Escapes `<` characters in JSON to safely use it in `<script>` tag.
15
+ */
16
+
17
+ function escapeJson(json) {
18
+ return JSON.stringify(json).replace(/</gu, '\\u003c');
19
+ }
20
+
21
+ function getAssetContent(filename) {
22
+ const assetPath = path.join(assetsRoot, filename);
23
+
24
+ if (!assetPath.startsWith(assetsRoot)) {
25
+ throw new Error(`"${filename}" is outside of the assets root`);
26
+ }
27
+
28
+ return fs.readFileSync(assetPath, 'utf8');
29
+ }
30
+
31
+ function html(strings, ...values) {
32
+ return strings.map((string, index) => `${string}${values[index] || ''}`).join('');
33
+ }
34
+
35
+ function getScript(filename, mode) {
36
+ if (mode === 'static') {
37
+ return `<!-- ${_.escape(filename)} -->
38
+ <script>${getAssetContent(filename)}</script>`;
39
+ } else {
40
+ return `<script src="${_.escape(filename)}"></script>`;
41
+ }
42
+ }
43
+
44
+ function renderViewer({
45
+ title,
46
+ enableWebSocket,
47
+ chartData,
48
+ defaultSizes,
49
+ mode
50
+ } = {}) {
51
+ return html`<!DOCTYPE html>
52
+ <html>
53
+ <head>
54
+ <meta charset="UTF-8"/>
55
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
56
+ <title>${_.escape(title)}</title>
57
+ <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" />
58
+
59
+ <script>
60
+ window.enableWebSocket = ${escapeJson(enableWebSocket)};
61
+ </script>
62
+ ${getScript('viewer.js', mode)}
63
+ </head>
64
+
65
+ <body>
66
+ <div id="app"></div>
67
+ <script>
68
+ window.chartData = ${escapeJson(chartData)};
69
+ window.defaultSizes = ${escapeJson(defaultSizes)};
70
+ </script>
71
+ </body>
72
+ </html>`;
73
+ }
@@ -69,8 +69,7 @@ class BaseFolder extends _Node.default {
69
69
 
70
70
  walk(walker, state = {}, deep = true) {
71
71
  let stopped = false;
72
-
73
- _lodash.default.each(this.children, child => {
72
+ Object.values(this.children).forEach(child => {
74
73
  if (deep && child.walk) {
75
74
  state = child.walk(walker, state, stop);
76
75
  } else {
@@ -79,7 +78,6 @@ class BaseFolder extends _Node.default {
79
78
 
80
79
  if (stopped) return false;
81
80
  });
82
-
83
81
  return state;
84
82
 
85
83
  function stop(finalState) {
@@ -26,7 +26,7 @@ class ConcatenatedModule extends _Module.default {
26
26
  }
27
27
 
28
28
  fillContentModules() {
29
- _lodash.default.each(this.data.modules, moduleData => this.addContentModule(moduleData));
29
+ this.data.modules.forEach(moduleData => this.addContentModule(moduleData));
30
30
  }
31
31
 
32
32
  addContentModule(moduleData) {
@@ -38,8 +38,7 @@ class ConcatenatedModule extends _Module.default {
38
38
 
39
39
  const [folders, fileName] = [pathParts.slice(0, -1), _lodash.default.last(pathParts)];
40
40
  let currentFolder = this;
41
-
42
- _lodash.default.each(folders, folderName => {
41
+ folders.forEach(folderName => {
43
42
  let childFolder = currentFolder.getChild(folderName);
44
43
 
45
44
  if (!childFolder) {
@@ -48,7 +47,6 @@ class ConcatenatedModule extends _Module.default {
48
47
 
49
48
  currentFolder = childFolder;
50
49
  });
51
-
52
50
  const module = new _ContentModule.default(fileName, moduleData, this);
53
51
  currentFolder.addChildModule(module);
54
52
  }
@@ -41,8 +41,7 @@ class Folder extends _BaseFolder.default {
41
41
 
42
42
  const [folders, fileName] = [pathParts.slice(0, -1), _lodash.default.last(pathParts)];
43
43
  let currentFolder = this;
44
-
45
- _lodash.default.each(folders, folderName => {
44
+ folders.forEach(folderName => {
46
45
  let childNode = currentFolder.getChild(folderName);
47
46
 
48
47
  if ( // Folder is not created yet
@@ -56,7 +55,6 @@ class Folder extends _BaseFolder.default {
56
55
 
57
56
  currentFolder = childNode;
58
57
  });
59
-
60
58
  const ModuleConstructor = moduleData.modules ? _ConcatenatedModule.default : _Module.default;
61
59
  const module = new ModuleConstructor(fileName, moduleData, this);
62
60
  currentFolder.addChildModule(module);
package/lib/utils.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  const {
4
- inspect
4
+ inspect,
5
+ types
5
6
  } = require('util');
6
7
 
7
8
  const _ = require('lodash');
@@ -17,11 +18,11 @@ function createAssetsFilter(excludePatterns) {
17
18
  pattern = new RegExp(pattern, 'u');
18
19
  }
19
20
 
20
- if (_.isRegExp(pattern)) {
21
+ if (types.isRegExp(pattern)) {
21
22
  return asset => pattern.test(asset);
22
23
  }
23
24
 
24
- if (!_.isFunction(pattern)) {
25
+ if (typeof pattern !== 'function') {
25
26
  throw new TypeError(`Pattern should be either string, RegExp or a function, but "${inspect(pattern, {
26
27
  depth: 0
27
28
  })}" got.`);
@@ -31,7 +32,7 @@ function createAssetsFilter(excludePatterns) {
31
32
  }).value();
32
33
 
33
34
  if (excludeFunctions.length) {
34
- return asset => _.every(excludeFunctions, fn => fn(asset) !== true);
35
+ return asset => excludeFunctions.every(fn => fn(asset) !== true);
35
36
  } else {
36
37
  return () => true;
37
38
  }
package/lib/viewer.js CHANGED
@@ -12,10 +12,6 @@ const _ = require('lodash');
12
12
 
13
13
  const express = require('express');
14
14
 
15
- const ejs = require('ejs');
16
-
17
- const mkdir = require('mkdirp');
18
-
19
15
  const {
20
16
  bold
21
17
  } = require('chalk');
@@ -28,8 +24,11 @@ const {
28
24
  open
29
25
  } = require('./utils');
30
26
 
27
+ const {
28
+ renderViewer
29
+ } = require('./template');
30
+
31
31
  const projectRoot = path.resolve(__dirname, '..');
32
- const assetsRoot = path.join(projectRoot, 'public');
33
32
 
34
33
  function resolveTitle(reportTitle) {
35
34
  if (typeof reportTitle === 'function') {
@@ -64,27 +63,20 @@ async function startServer(bundleStats, opts) {
64
63
  };
65
64
  let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
66
65
  if (!chartData) return;
67
- const app = express(); // Explicitly using our `ejs` dependency to render templates
68
- // Fixes #17
69
-
70
- app.engine('ejs', require('ejs').renderFile);
71
- app.set('view engine', 'ejs');
72
- app.set('views', `${projectRoot}/views`);
66
+ const app = express();
73
67
  app.use(express.static(`${projectRoot}/public`));
74
- app.use('/', (req, res) => {
75
- res.render('viewer', {
68
+ app.get('/', (req, res) => {
69
+ res.writeHead(200, {
70
+ 'Content-Type': 'text/html'
71
+ });
72
+ const html = renderViewer({
76
73
  mode: 'server',
77
74
  title: resolveTitle(reportTitle),
78
-
79
- get chartData() {
80
- return chartData;
81
- },
82
-
75
+ chartData,
83
76
  defaultSizes,
84
- enableWebSocket: true,
85
- // Helpers
86
- escapeJson
77
+ enableWebSocket: true
87
78
  });
79
+ return res.end(html);
88
80
  });
89
81
  const server = http.createServer(app);
90
82
  await new Promise(resolve => {
@@ -144,39 +136,23 @@ async function generateReport(bundleStats, opts) {
144
136
  excludeAssets
145
137
  }, bundleStats, bundleDir);
146
138
  if (!chartData) return;
147
- await new Promise((resolve, reject) => {
148
- ejs.renderFile(`${projectRoot}/views/viewer.ejs`, {
149
- mode: 'static',
150
- title: resolveTitle(reportTitle),
151
- chartData,
152
- defaultSizes,
153
- enableWebSocket: false,
154
- // Helpers
155
- assetContent: getAssetContent,
156
- escapeJson
157
- }, (err, reportHtml) => {
158
- try {
159
- if (err) {
160
- logger.error(err);
161
- reject(err);
162
- return;
163
- }
164
-
165
- const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
166
- mkdir.sync(path.dirname(reportFilepath));
167
- fs.writeFileSync(reportFilepath, reportHtml);
168
- logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
169
-
170
- if (openBrowser) {
171
- open(`file://${reportFilepath}`, logger);
172
- }
173
-
174
- resolve();
175
- } catch (e) {
176
- reject(e);
177
- }
178
- });
139
+ const reportHtml = renderViewer({
140
+ mode: 'static',
141
+ title: resolveTitle(reportTitle),
142
+ chartData,
143
+ defaultSizes,
144
+ enableWebSocket: false
145
+ });
146
+ const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
147
+ fs.mkdirSync(path.dirname(reportFilepath), {
148
+ recursive: true
179
149
  });
150
+ fs.writeFileSync(reportFilepath, reportHtml);
151
+ logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
152
+
153
+ if (openBrowser) {
154
+ open(`file://${reportFilepath}`, logger);
155
+ }
180
156
  }
181
157
 
182
158
  async function generateJSONReport(bundleStats, opts) {
@@ -191,29 +167,13 @@ async function generateJSONReport(bundleStats, opts) {
191
167
  excludeAssets
192
168
  }, bundleStats, bundleDir);
193
169
  if (!chartData) return;
194
- mkdir.sync(path.dirname(reportFilename));
195
- fs.writeFileSync(reportFilename, JSON.stringify(chartData));
170
+ await fs.promises.mkdir(path.dirname(reportFilename), {
171
+ recursive: true
172
+ });
173
+ await fs.promises.writeFile(reportFilename, JSON.stringify(chartData));
196
174
  logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`);
197
175
  }
198
176
 
199
- function getAssetContent(filename) {
200
- const assetPath = path.join(assetsRoot, filename);
201
-
202
- if (!assetPath.startsWith(assetsRoot)) {
203
- throw new Error(`"${filename}" is outside of the assets root`);
204
- }
205
-
206
- return fs.readFileSync(assetPath, 'utf8');
207
- }
208
- /**
209
- * Escapes `<` characters in JSON to safely use it in `<script>` tag.
210
- */
211
-
212
-
213
- function escapeJson(json) {
214
- return JSON.stringify(json).replace(/</gu, '\\u003c');
215
- }
216
-
217
177
  function getChartData(analyzerOpts, ...args) {
218
178
  let chartData;
219
179
  const {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webpack-bundle-analyzer",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Webpack plugin and CLI utility that represents bundle content as convenient interactive zoomable treemap",
5
5
  "author": "Yury Grunin <grunin.ya@ya.ru>",
6
6
  "license": "MIT",
@@ -30,20 +30,17 @@
30
30
  "files": [
31
31
  "public",
32
32
  "lib",
33
- "src",
34
- "views"
33
+ "src"
35
34
  ],
36
35
  "dependencies": {
37
36
  "acorn": "^8.0.4",
38
37
  "acorn-walk": "^8.0.0",
39
38
  "chalk": "^4.1.0",
40
39
  "commander": "^6.2.0",
41
- "ejs": "^3.1.5",
42
40
  "express": "^4.17.1",
43
41
  "filesize": "^6.1.0",
44
- "gzip-size": "^5.1.1",
42
+ "gzip-size": "^6.0.0",
45
43
  "lodash": "^4.17.20",
46
- "mkdirp": "^1.0.4",
47
44
  "opener": "^1.5.2",
48
45
  "ws": "^7.3.1"
49
46
  },
@@ -1,5 +1,5 @@
1
+ const fs = require('fs');
1
2
  const path = require('path');
2
- const mkdir = require('mkdirp');
3
3
  const {bold} = require('chalk');
4
4
 
5
5
  const Logger = require('./Logger');
@@ -80,7 +80,7 @@ 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
86
  await writeStats(stats, 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
 
@@ -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
@@ -5,16 +5,14 @@ const http = require('http');
5
5
  const WebSocket = require('ws');
6
6
  const _ = require('lodash');
7
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') {
@@ -51,24 +49,18 @@ async function startServer(bundleStats, opts) {
51
49
  if (!chartData) return;
52
50
 
53
51
  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
52
  app.use(express.static(`${projectRoot}/public`));
61
53
 
62
- app.use('/', (req, res) => {
63
- res.render('viewer', {
54
+ app.get('/', (req, res) => {
55
+ res.writeHead(200, {'Content-Type': 'text/html'});
56
+ const html = renderViewer({
64
57
  mode: 'server',
65
58
  title: resolveTitle(reportTitle),
66
- get chartData() { return chartData },
59
+ chartData,
67
60
  defaultSizes,
68
- enableWebSocket: true,
69
- // Helpers
70
- escapeJson
61
+ enableWebSocket: true
71
62
  });
63
+ return res.end(html);
72
64
  });
73
65
 
74
66
  const server = http.createServer(app);
@@ -140,44 +132,23 @@ async function generateReport(bundleStats, opts) {
140
132
 
141
133
  if (!chartData) return;
142
134
 
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
- );
135
+ const reportHtml = renderViewer({
136
+ mode: 'static',
137
+ title: resolveTitle(reportTitle),
138
+ chartData,
139
+ defaultSizes,
140
+ enableWebSocket: false
180
141
  });
142
+ const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
143
+
144
+ fs.mkdirSync(path.dirname(reportFilepath), {recursive: true});
145
+ fs.writeFileSync(reportFilepath, reportHtml);
146
+
147
+ logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
148
+
149
+ if (openBrowser) {
150
+ open(`file://${reportFilepath}`, logger);
151
+ }
181
152
  }
182
153
 
183
154
  async function generateJSONReport(bundleStats, opts) {
@@ -187,29 +158,12 @@ async function generateJSONReport(bundleStats, opts) {
187
158
 
188
159
  if (!chartData) return;
189
160
 
190
- mkdir.sync(path.dirname(reportFilename));
191
- fs.writeFileSync(reportFilename, JSON.stringify(chartData));
161
+ await fs.promises.mkdir(path.dirname(reportFilename), {recursive: true});
162
+ await fs.promises.writeFile(reportFilename, JSON.stringify(chartData));
192
163
 
193
164
  logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`);
194
165
  }
195
166
 
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
167
  function getChartData(analyzerOpts, ...args) {
214
168
  let chartData;
215
169
  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
- <% } %>