webpack-bundle-analyzer 4.0.0-rc1 → 4.3.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 +24 -0
- package/lib/BundleAnalyzerPlugin.js +9 -12
- package/lib/analyzer.js +27 -27
- package/lib/bin/analyzer.js +1 -3
- package/lib/parseUtils.js +13 -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 +2 -4
- package/lib/tree/Folder.js +1 -3
- package/lib/utils.js +5 -4
- package/lib/viewer.js +46 -81
- package/package.json +7 -10
- package/src/BundleAnalyzerPlugin.js +4 -11
- package/src/analyzer.js +27 -23
- package/src/bin/analyzer.js +1 -2
- package/src/parseUtils.js +14 -12
- 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 +4 -4
- package/src/viewer.js +38 -80
- package/views/script.ejs +0 -8
package/lib/viewer.js
CHANGED
|
@@ -8,13 +8,9 @@ const http = require('http');
|
|
|
8
8
|
|
|
9
9
|
const WebSocket = require('ws');
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const express = require('express');
|
|
14
|
-
|
|
15
|
-
const ejs = require('ejs');
|
|
11
|
+
const sirv = require('sirv');
|
|
16
12
|
|
|
17
|
-
const
|
|
13
|
+
const _ = require('lodash');
|
|
18
14
|
|
|
19
15
|
const {
|
|
20
16
|
bold
|
|
@@ -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,29 +63,27 @@ async function startServer(bundleStats, opts) {
|
|
|
64
63
|
};
|
|
65
64
|
let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
|
|
66
65
|
if (!chartData) return;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
66
|
+
const sirvMiddleware = sirv(`${projectRoot}/public`, {
|
|
67
|
+
// disables caching and traverse the file system on every request
|
|
68
|
+
dev: true
|
|
69
|
+
});
|
|
70
|
+
const server = http.createServer((req, res) => {
|
|
71
|
+
if (req.method === 'GET' && req.url === '/') {
|
|
72
|
+
const html = renderViewer({
|
|
73
|
+
mode: 'server',
|
|
74
|
+
title: resolveTitle(reportTitle),
|
|
75
|
+
chartData,
|
|
76
|
+
defaultSizes,
|
|
77
|
+
enableWebSocket: true
|
|
78
|
+
});
|
|
79
|
+
res.writeHead(200, {
|
|
80
|
+
'Content-Type': 'text/html'
|
|
81
|
+
});
|
|
82
|
+
res.end(html);
|
|
83
|
+
} else {
|
|
84
|
+
sirvMiddleware(req, res);
|
|
85
|
+
}
|
|
88
86
|
});
|
|
89
|
-
const server = http.createServer(app);
|
|
90
87
|
await new Promise(resolve => {
|
|
91
88
|
server.listen(port, host, () => {
|
|
92
89
|
resolve();
|
|
@@ -144,39 +141,23 @@ async function generateReport(bundleStats, opts) {
|
|
|
144
141
|
excludeAssets
|
|
145
142
|
}, bundleStats, bundleDir);
|
|
146
143
|
if (!chartData) return;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
});
|
|
144
|
+
const reportHtml = renderViewer({
|
|
145
|
+
mode: 'static',
|
|
146
|
+
title: resolveTitle(reportTitle),
|
|
147
|
+
chartData,
|
|
148
|
+
defaultSizes,
|
|
149
|
+
enableWebSocket: false
|
|
150
|
+
});
|
|
151
|
+
const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
|
|
152
|
+
fs.mkdirSync(path.dirname(reportFilepath), {
|
|
153
|
+
recursive: true
|
|
179
154
|
});
|
|
155
|
+
fs.writeFileSync(reportFilepath, reportHtml);
|
|
156
|
+
logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
|
|
157
|
+
|
|
158
|
+
if (openBrowser) {
|
|
159
|
+
open(`file://${reportFilepath}`, logger);
|
|
160
|
+
}
|
|
180
161
|
}
|
|
181
162
|
|
|
182
163
|
async function generateJSONReport(bundleStats, opts) {
|
|
@@ -191,29 +172,13 @@ async function generateJSONReport(bundleStats, opts) {
|
|
|
191
172
|
excludeAssets
|
|
192
173
|
}, bundleStats, bundleDir);
|
|
193
174
|
if (!chartData) return;
|
|
194
|
-
mkdir
|
|
195
|
-
|
|
175
|
+
await fs.promises.mkdir(path.dirname(reportFilename), {
|
|
176
|
+
recursive: true
|
|
177
|
+
});
|
|
178
|
+
await fs.promises.writeFile(reportFilename, JSON.stringify(chartData));
|
|
196
179
|
logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`);
|
|
197
180
|
}
|
|
198
181
|
|
|
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
182
|
function getChartData(analyzerOpts, ...args) {
|
|
218
183
|
let chartData;
|
|
219
184
|
const {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webpack-bundle-analyzer",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.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",
|
|
@@ -25,27 +25,22 @@
|
|
|
25
25
|
"lint": "eslint --ext js,jsx .",
|
|
26
26
|
"install-test-webpack-versions": "./bin/install-test-webpack-versions.sh",
|
|
27
27
|
"test": "npm run install-test-webpack-versions && mocha --exit --require @babel/register",
|
|
28
|
-
"test-dev": "npm run install-test-webpack-versions && mocha --watch --require @babel/register"
|
|
28
|
+
"test-dev": "npm run install-test-webpack-versions && mocha --watch --watch-ignore test/output --require @babel/register"
|
|
29
29
|
},
|
|
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
|
-
"bfj": "^7.0.2",
|
|
40
38
|
"chalk": "^4.1.0",
|
|
41
39
|
"commander": "^6.2.0",
|
|
42
|
-
"
|
|
43
|
-
"express": "^4.17.1",
|
|
44
|
-
"filesize": "^6.1.0",
|
|
45
|
-
"gzip-size": "^5.1.1",
|
|
40
|
+
"gzip-size": "^6.0.0",
|
|
46
41
|
"lodash": "^4.17.20",
|
|
47
|
-
"mkdirp": "^1.0.4",
|
|
48
42
|
"opener": "^1.5.2",
|
|
43
|
+
"sirv": "^1.0.7",
|
|
49
44
|
"ws": "^7.3.1"
|
|
50
45
|
},
|
|
51
46
|
"devDependencies": {
|
|
@@ -73,6 +68,8 @@
|
|
|
73
68
|
"eslint-config-th0r-react": "2.0.1",
|
|
74
69
|
"eslint-plugin-react": "7.21.5",
|
|
75
70
|
"exports-loader": "1.1.1",
|
|
71
|
+
"filesize": "^6.1.0",
|
|
72
|
+
"globby": "11.0.1",
|
|
76
73
|
"gulp": "4.0.2",
|
|
77
74
|
"gulp-babel": "8.0.0",
|
|
78
75
|
"mobx": "5.15.7",
|
|
@@ -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, '');
|
|
@@ -77,7 +82,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
|
|
80
|
-
|
|
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 =
|
|
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) : [];
|
|
@@ -115,7 +120,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
// Webpack 5 changed bundle format and now entry modules are concatenated and located at the end
|
|
123
|
+
// Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
|
|
119
124
|
// Because of this they basically become a concatenated module, for which we can't even precisely determine its
|
|
120
125
|
// parsed source as it's located in the same scope as all Webpack runtime helpers.
|
|
121
126
|
if (unparsedEntryModules.length && assetSources) {
|
|
@@ -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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
|
190
|
-
|
|
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
|
-
|
|
209
|
+
modules.forEach(module => root.addModule(module));
|
|
206
210
|
root.mergeNestedFolders();
|
|
207
211
|
|
|
208
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
|
@@ -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
|
-
|
|
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 =
|
|
165
|
-
.
|
|
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
|
-
|
|
218
|
-
.map(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
346
|
-
if (
|
|
347
|
-
|
|
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
|
-
|
|
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 (
|