webpack-bundle-analyzer 3.9.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/lib/BundleAnalyzerPlugin.js +55 -85
- package/lib/analyzer.js +78 -35
- package/lib/bin/analyzer.js +1 -3
- package/lib/parseUtils.js +83 -10
- package/lib/statsUtils.js +90 -0
- package/lib/template.js +73 -0
- package/lib/tree/BaseFolder.js +1 -3
- package/lib/tree/ConcatenatedModule.js +4 -12
- package/lib/tree/ContentFolder.js +2 -8
- package/lib/tree/ContentModule.js +2 -8
- package/lib/tree/Folder.js +3 -11
- package/lib/utils.js +19 -4
- package/lib/viewer.js +121 -184
- package/package.json +47 -49
- package/public/viewer.js +4 -14
- package/public/viewer.js.LICENSE.txt +5 -0
- package/public/viewer.js.map +1 -1
- package/src/BundleAnalyzerPlugin.js +4 -11
- package/src/analyzer.js +76 -31
- package/src/bin/analyzer.js +1 -2
- package/src/parseUtils.js +96 -11
- package/src/statsUtils.js +82 -0
- package/{views/viewer.ejs → src/template.js} +50 -7
- package/src/tree/BaseFolder.js +1 -1
- package/src/tree/ConcatenatedModule.js +2 -2
- package/src/tree/Folder.js +1 -1
- package/src/utils.js +16 -4
- package/src/viewer.js +27 -73
- package/views/script.ejs +0 -8
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const
|
|
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
|
|
83
|
+
await fs.promises.mkdir(path.dirname(statsFilepath), {recursive: true});
|
|
84
84
|
|
|
85
85
|
try {
|
|
86
|
-
await
|
|
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
|
-
|
|
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 =
|
|
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, '');
|
|
@@ -76,8 +81,8 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
76
81
|
continue;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
bundlesSources[statAsset.name] = bundleInfo
|
|
80
|
-
|
|
84
|
+
bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
|
|
85
|
+
Object.assign(parsedModules, bundleInfo.modules);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
if (_.isEmpty(bundlesSources)) {
|
|
@@ -87,43 +92,73 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
const assets =
|
|
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) : [];
|
|
94
99
|
const asset = result[statAsset.name] = _.pick(statAsset, 'size');
|
|
100
|
+
const assetSources = bundlesSources && _.has(bundlesSources, statAsset.name) ?
|
|
101
|
+
bundlesSources[statAsset.name] : null;
|
|
95
102
|
|
|
96
|
-
if (
|
|
97
|
-
asset.parsedSize = Buffer.byteLength(
|
|
98
|
-
asset.gzipSize = gzipSize.sync(
|
|
103
|
+
if (assetSources) {
|
|
104
|
+
asset.parsedSize = Buffer.byteLength(assetSources.src);
|
|
105
|
+
asset.gzipSize = gzipSize.sync(assetSources.src);
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
// Picking modules from current bundle script
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
const assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule));
|
|
110
|
+
|
|
111
|
+
// Adding parsed sources
|
|
112
|
+
if (parsedModules) {
|
|
113
|
+
const unparsedEntryModules = [];
|
|
114
|
+
|
|
115
|
+
for (const statModule of assetModules) {
|
|
116
|
+
if (parsedModules[statModule.id]) {
|
|
106
117
|
statModule.parsedSrc = parsedModules[statModule.id];
|
|
118
|
+
} else if (isEntryModule(statModule)) {
|
|
119
|
+
unparsedEntryModules.push(statModule);
|
|
107
120
|
}
|
|
108
|
-
}
|
|
121
|
+
}
|
|
109
122
|
|
|
123
|
+
// Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
|
|
124
|
+
// Because of this they basically become a concatenated module, for which we can't even precisely determine its
|
|
125
|
+
// parsed source as it's located in the same scope as all Webpack runtime helpers.
|
|
126
|
+
if (unparsedEntryModules.length && assetSources) {
|
|
127
|
+
if (unparsedEntryModules.length === 1) {
|
|
128
|
+
// So if there is only one entry we consider its parsed source to be all the bundle code excluding code
|
|
129
|
+
// from parsed modules.
|
|
130
|
+
unparsedEntryModules[0].parsedSrc = assetSources.runtimeSrc;
|
|
131
|
+
} else {
|
|
132
|
+
// If there are multiple entry points we move all of them under synthetic concatenated module.
|
|
133
|
+
_.pullAll(assetModules, unparsedEntryModules);
|
|
134
|
+
assetModules.unshift({
|
|
135
|
+
identifier: './entry modules',
|
|
136
|
+
name: './entry modules',
|
|
137
|
+
modules: unparsedEntryModules,
|
|
138
|
+
size: unparsedEntryModules.reduce((totalSize, module) => totalSize + module.size, 0),
|
|
139
|
+
parsedSrc: assetSources.runtimeSrc
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
asset.modules = assetModules;
|
|
110
146
|
asset.tree = createModulesTree(asset.modules);
|
|
147
|
+
return result;
|
|
111
148
|
}, {});
|
|
112
149
|
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
}, []);
|
|
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
|
+
}));
|
|
127
162
|
}
|
|
128
163
|
|
|
129
164
|
function readStatsFromFile(filename) {
|
|
@@ -133,7 +168,7 @@ function readStatsFromFile(filename) {
|
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
function getChildAssetBundles(bundleStats, assetName) {
|
|
136
|
-
return
|
|
171
|
+
return (bundleStats.children || []).find((c) =>
|
|
137
172
|
_(c.assetsByChunkName)
|
|
138
173
|
.values()
|
|
139
174
|
.flatten()
|
|
@@ -148,20 +183,30 @@ function getBundleModules(bundleStats) {
|
|
|
148
183
|
.compact()
|
|
149
184
|
.flatten()
|
|
150
185
|
.uniqBy('id')
|
|
186
|
+
// Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
|
|
187
|
+
.reject(isRuntimeModule)
|
|
151
188
|
.value();
|
|
152
189
|
}
|
|
153
190
|
|
|
154
191
|
function assetHasModule(statAsset, statModule) {
|
|
155
192
|
// Checking if this module is the part of asset chunks
|
|
156
|
-
return
|
|
157
|
-
|
|
193
|
+
return statModule.chunks.some(moduleChunk =>
|
|
194
|
+
statAsset.chunks.includes(moduleChunk)
|
|
158
195
|
);
|
|
159
196
|
}
|
|
160
197
|
|
|
198
|
+
function isEntryModule(statModule) {
|
|
199
|
+
return statModule.depth === 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isRuntimeModule(statModule) {
|
|
203
|
+
return statModule.moduleType === 'runtime';
|
|
204
|
+
}
|
|
205
|
+
|
|
161
206
|
function createModulesTree(modules) {
|
|
162
207
|
const root = new Folder('.');
|
|
163
208
|
|
|
164
|
-
|
|
209
|
+
modules.forEach(module => root.addModule(module));
|
|
165
210
|
root.mergeNestedFolders();
|
|
166
211
|
|
|
167
212
|
return root;
|
package/src/bin/analyzer.js
CHANGED
|
@@ -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${
|
|
159
|
+
return `\n${' '.repeat(28)}${str}`;
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
function array() {
|
package/src/parseUtils.js
CHANGED
|
@@ -18,13 +18,57 @@ function parseBundle(bundlePath) {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const walkState = {
|
|
21
|
-
locations: null
|
|
21
|
+
locations: null,
|
|
22
|
+
expressionStatementDepth: 0
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
walk.recursive(
|
|
25
26
|
ast,
|
|
26
27
|
walkState,
|
|
27
28
|
{
|
|
29
|
+
ExpressionStatement(node, state, c) {
|
|
30
|
+
if (state.locations) return;
|
|
31
|
+
|
|
32
|
+
state.expressionStatementDepth++;
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
// Webpack 5 stores modules in the the top-level IIFE
|
|
36
|
+
state.expressionStatementDepth === 1 &&
|
|
37
|
+
ast.body.includes(node) &&
|
|
38
|
+
isIIFE(node)
|
|
39
|
+
) {
|
|
40
|
+
const fn = getIIFECallExpression(node);
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
// It should not contain neither arguments
|
|
44
|
+
fn.arguments.length === 0 &&
|
|
45
|
+
// ...nor parameters
|
|
46
|
+
fn.callee.params.length === 0
|
|
47
|
+
) {
|
|
48
|
+
// Modules are stored in the very first variable declaration as hash
|
|
49
|
+
const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === 'VariableDeclaration');
|
|
50
|
+
|
|
51
|
+
if (firstVariableDeclaration) {
|
|
52
|
+
for (const declaration of firstVariableDeclaration.declarations) {
|
|
53
|
+
if (declaration.init) {
|
|
54
|
+
state.locations = getModulesLocations(declaration.init);
|
|
55
|
+
|
|
56
|
+
if (state.locations) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!state.locations) {
|
|
66
|
+
c(node.expression, state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
state.expressionStatementDepth--;
|
|
70
|
+
},
|
|
71
|
+
|
|
28
72
|
AssignmentExpression(node, state) {
|
|
29
73
|
if (state.locations) return;
|
|
30
74
|
|
|
@@ -41,6 +85,7 @@ function parseBundle(bundlePath) {
|
|
|
41
85
|
state.locations = getModulesLocations(right);
|
|
42
86
|
}
|
|
43
87
|
},
|
|
88
|
+
|
|
44
89
|
CallExpression(node, state, c) {
|
|
45
90
|
if (state.locations) return;
|
|
46
91
|
|
|
@@ -90,7 +135,7 @@ function parseBundle(bundlePath) {
|
|
|
90
135
|
|
|
91
136
|
// Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
|
|
92
137
|
// features (e.g. `umd` library output) can wrap modules list into additional IIFE.
|
|
93
|
-
|
|
138
|
+
args.forEach(arg => c(arg, state));
|
|
94
139
|
}
|
|
95
140
|
}
|
|
96
141
|
);
|
|
@@ -106,11 +151,48 @@ function parseBundle(bundlePath) {
|
|
|
106
151
|
}
|
|
107
152
|
|
|
108
153
|
return {
|
|
154
|
+
modules,
|
|
109
155
|
src: content,
|
|
110
|
-
|
|
156
|
+
runtimeSrc: getBundleRuntime(content, walkState.locations)
|
|
111
157
|
};
|
|
112
158
|
}
|
|
113
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Returns bundle source except modules
|
|
162
|
+
*/
|
|
163
|
+
function getBundleRuntime(content, modulesLocations) {
|
|
164
|
+
const sortedLocations = Object.values(modulesLocations || {})
|
|
165
|
+
.sort((a, b) => a.start - b.start);
|
|
166
|
+
|
|
167
|
+
let result = '';
|
|
168
|
+
let lastIndex = 0;
|
|
169
|
+
|
|
170
|
+
for (const {start, end} of sortedLocations) {
|
|
171
|
+
result += content.slice(lastIndex, start);
|
|
172
|
+
lastIndex = end;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result + content.slice(lastIndex, content.length);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isIIFE(node) {
|
|
179
|
+
return (
|
|
180
|
+
node.type === 'ExpressionStatement' &&
|
|
181
|
+
(
|
|
182
|
+
node.expression.type === 'CallExpression' ||
|
|
183
|
+
(node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression')
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getIIFECallExpression(node) {
|
|
189
|
+
if (node.expression.type === 'UnaryExpression') {
|
|
190
|
+
return node.expression.argument;
|
|
191
|
+
} else {
|
|
192
|
+
return node.expression;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
114
196
|
function isModulesList(node) {
|
|
115
197
|
return (
|
|
116
198
|
isSimpleModulesList(node) ||
|
|
@@ -131,8 +213,8 @@ function isSimpleModulesList(node) {
|
|
|
131
213
|
function isModulesHash(node) {
|
|
132
214
|
return (
|
|
133
215
|
node.type === 'ObjectExpression' &&
|
|
134
|
-
|
|
135
|
-
.map(
|
|
216
|
+
node.properties
|
|
217
|
+
.map(node => node.value)
|
|
136
218
|
.every(isModuleWrapper)
|
|
137
219
|
);
|
|
138
220
|
}
|
|
@@ -140,7 +222,7 @@ function isModulesHash(node) {
|
|
|
140
222
|
function isModulesArray(node) {
|
|
141
223
|
return (
|
|
142
224
|
node.type === 'ArrayExpression' &&
|
|
143
|
-
|
|
225
|
+
node.elements.every(elem =>
|
|
144
226
|
// Some of array items may be skipped because there is no module with such id
|
|
145
227
|
!elem ||
|
|
146
228
|
isModuleWrapper(elem)
|
|
@@ -193,7 +275,7 @@ function isChunkIds(node) {
|
|
|
193
275
|
// Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
|
|
194
276
|
return (
|
|
195
277
|
node.type === 'ArrayExpression' &&
|
|
196
|
-
|
|
278
|
+
node.elements.every(isModuleId)
|
|
197
279
|
);
|
|
198
280
|
}
|
|
199
281
|
|
|
@@ -238,10 +320,11 @@ function getModulesLocations(node) {
|
|
|
238
320
|
// Modules hash
|
|
239
321
|
const modulesNodes = node.properties;
|
|
240
322
|
|
|
241
|
-
return
|
|
323
|
+
return modulesNodes.reduce((result, moduleNode) => {
|
|
242
324
|
const moduleId = moduleNode.key.name || moduleNode.key.value;
|
|
243
325
|
|
|
244
326
|
result[moduleId] = getModuleLocation(moduleNode.value);
|
|
327
|
+
return result;
|
|
245
328
|
}, {});
|
|
246
329
|
}
|
|
247
330
|
|
|
@@ -259,9 +342,11 @@ function getModulesLocations(node) {
|
|
|
259
342
|
node.arguments[0].elements :
|
|
260
343
|
node.elements;
|
|
261
344
|
|
|
262
|
-
return
|
|
263
|
-
if (
|
|
264
|
-
|
|
345
|
+
return modulesNodes.reduce((result, moduleNode, i) => {
|
|
346
|
+
if (moduleNode) {
|
|
347
|
+
result[i + minId] = getModuleLocation(moduleNode);
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
265
350
|
}, {});
|
|
266
351
|
}
|
|
267
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
|
-
|
|
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
|
|
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 =
|
|
52
|
+
window.enableWebSocket = ${escapeJson(enableWebSocket)};
|
|
11
53
|
</script>
|
|
12
|
-
|
|
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 =
|
|
19
|
-
window.defaultSizes =
|
|
60
|
+
window.chartData = ${escapeJson(chartData)};
|
|
61
|
+
window.defaultSizes = ${escapeJson(defaultSizes)};
|
|
20
62
|
</script>
|
|
21
63
|
</body>
|
|
22
|
-
</html
|
|
64
|
+
</html>`;
|
|
65
|
+
}
|
package/src/tree/BaseFolder.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
folders.forEach(folderName => {
|
|
32
32
|
let childFolder = currentFolder.getChild(folderName);
|
|
33
33
|
|
|
34
34
|
if (!childFolder) {
|
package/src/tree/Folder.js
CHANGED
|
@@ -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
|
-
|
|
33
|
+
folders.forEach(folderName => {
|
|
34
34
|
let childNode = currentFolder.getChild(folderName);
|
|
35
35
|
|
|
36
36
|
if (
|
package/src/utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const {inspect} = require('util');
|
|
1
|
+
const {inspect, types} = require('util');
|
|
2
2
|
const _ = require('lodash');
|
|
3
|
+
const opener = require('opener');
|
|
3
4
|
|
|
4
5
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
5
6
|
|
|
@@ -14,11 +15,11 @@ function createAssetsFilter(excludePatterns) {
|
|
|
14
15
|
pattern = new RegExp(pattern, 'u');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
if (
|
|
18
|
+
if (types.isRegExp(pattern)) {
|
|
18
19
|
return (asset) => pattern.test(asset);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
if (
|
|
22
|
+
if (typeof pattern !== 'function') {
|
|
22
23
|
throw new TypeError(
|
|
23
24
|
`Pattern should be either string, RegExp or a function, but "${inspect(pattern, {depth: 0})}" got.`
|
|
24
25
|
);
|
|
@@ -29,7 +30,7 @@ function createAssetsFilter(excludePatterns) {
|
|
|
29
30
|
.value();
|
|
30
31
|
|
|
31
32
|
if (excludeFunctions.length) {
|
|
32
|
-
return (asset) =>
|
|
33
|
+
return (asset) => excludeFunctions.every(fn => fn(asset) !== true);
|
|
33
34
|
} else {
|
|
34
35
|
return () => true;
|
|
35
36
|
}
|
|
@@ -51,3 +52,14 @@ exports.defaultTitle = function () {
|
|
|
51
52
|
|
|
52
53
|
return `${process.env.npm_package_name || 'Webpack Bundle Analyzer'} [${currentTime}]`;
|
|
53
54
|
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calls opener on a URI, but silently try / catches it.
|
|
58
|
+
*/
|
|
59
|
+
exports.open = function (uri, logger) {
|
|
60
|
+
try {
|
|
61
|
+
opener(uri);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger.debug(`Opener failed to open "${uri}":\n${err}`);
|
|
64
|
+
}
|
|
65
|
+
};
|