webpack-bundle-analyzer 4.4.2 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,154 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const {bold} = require('chalk');
4
-
5
- const Logger = require('./Logger');
6
- const viewer = require('./viewer');
7
- const utils = require('./utils');
8
- const {writeStats} = require('./statsUtils');
9
-
10
- class BundleAnalyzerPlugin {
11
- constructor(opts = {}) {
12
- this.opts = {
13
- analyzerMode: 'server',
14
- analyzerHost: '127.0.0.1',
15
- reportFilename: null,
16
- reportTitle: utils.defaultTitle,
17
- defaultSizes: 'parsed',
18
- openAnalyzer: true,
19
- generateStatsFile: false,
20
- statsFilename: 'stats.json',
21
- statsOptions: null,
22
- excludeAssets: null,
23
- logLevel: 'info',
24
- // deprecated
25
- startAnalyzer: true,
26
- ...opts,
27
- analyzerPort: 'analyzerPort' in opts ? (opts.analyzerPort === 'auto' ? 0 : opts.analyzerPort) : 8888
28
- };
29
-
30
- this.server = null;
31
- this.logger = new Logger(this.opts.logLevel);
32
- }
33
-
34
- apply(compiler) {
35
- this.compiler = compiler;
36
-
37
- const done = (stats, callback) => {
38
- callback = callback || (() => {});
39
-
40
- const actions = [];
41
-
42
- if (this.opts.generateStatsFile) {
43
- actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions)));
44
- }
45
-
46
- // Handling deprecated `startAnalyzer` flag
47
- if (this.opts.analyzerMode === 'server' && !this.opts.startAnalyzer) {
48
- this.opts.analyzerMode = 'disabled';
49
- }
50
-
51
- if (this.opts.analyzerMode === 'server') {
52
- actions.push(() => this.startAnalyzerServer(stats.toJson()));
53
- } else if (this.opts.analyzerMode === 'static') {
54
- actions.push(() => this.generateStaticReport(stats.toJson()));
55
- } else if (this.opts.analyzerMode === 'json') {
56
- actions.push(() => this.generateJSONReport(stats.toJson()));
57
- }
58
-
59
- if (actions.length) {
60
- // Making analyzer logs to be after all webpack logs in the console
61
- setImmediate(async () => {
62
- try {
63
- await Promise.all(actions.map(action => action()));
64
- callback();
65
- } catch (e) {
66
- callback(e);
67
- }
68
- });
69
- } else {
70
- callback();
71
- }
72
- };
73
-
74
- if (compiler.hooks) {
75
- compiler.hooks.done.tapAsync('webpack-bundle-analyzer', done);
76
- } else {
77
- compiler.plugin('done', done);
78
- }
79
- }
80
-
81
- async generateStatsFile(stats) {
82
- const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename);
83
- await fs.promises.mkdir(path.dirname(statsFilepath), {recursive: true});
84
-
85
- try {
86
- await writeStats(stats, statsFilepath);
87
-
88
- this.logger.info(
89
- `${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`
90
- );
91
- } catch (error) {
92
- this.logger.error(
93
- `${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}`
94
- );
95
- }
96
- }
97
-
98
- async startAnalyzerServer(stats) {
99
- if (this.server) {
100
- (await this.server).updateChartData(stats);
101
- } else {
102
- this.server = viewer.startServer(stats, {
103
- openBrowser: this.opts.openAnalyzer,
104
- host: this.opts.analyzerHost,
105
- port: this.opts.analyzerPort,
106
- reportTitle: this.opts.reportTitle,
107
- bundleDir: this.getBundleDirFromCompiler(),
108
- logger: this.logger,
109
- defaultSizes: this.opts.defaultSizes,
110
- excludeAssets: this.opts.excludeAssets
111
- });
112
- }
113
- }
114
-
115
- async generateJSONReport(stats) {
116
- await viewer.generateJSONReport(stats, {
117
- reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'),
118
- bundleDir: this.getBundleDirFromCompiler(),
119
- logger: this.logger,
120
- excludeAssets: this.opts.excludeAssets
121
- });
122
- }
123
-
124
- async generateStaticReport(stats) {
125
- await viewer.generateReport(stats, {
126
- openBrowser: this.opts.openAnalyzer,
127
- reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'),
128
- reportTitle: this.opts.reportTitle,
129
- bundleDir: this.getBundleDirFromCompiler(),
130
- logger: this.logger,
131
- defaultSizes: this.opts.defaultSizes,
132
- excludeAssets: this.opts.excludeAssets
133
- });
134
- }
135
-
136
- getBundleDirFromCompiler() {
137
- if (typeof this.compiler.outputFileSystem.constructor === 'undefined') {
138
- return this.compiler.outputPath;
139
- }
140
- switch (this.compiler.outputFileSystem.constructor.name) {
141
- case 'MemoryFileSystem':
142
- return null;
143
- // Detect AsyncMFS used by Nuxt 2.5 that replaces webpack's MFS during development
144
- // Related: #274
145
- case 'AsyncMFS':
146
- return null;
147
- default:
148
- return this.compiler.outputPath;
149
- }
150
- }
151
-
152
- }
153
-
154
- module.exports = BundleAnalyzerPlugin;
package/src/Logger.js DELETED
@@ -1,51 +0,0 @@
1
- const LEVELS = [
2
- 'debug',
3
- 'info',
4
- 'warn',
5
- 'error',
6
- 'silent'
7
- ];
8
-
9
- const LEVEL_TO_CONSOLE_METHOD = new Map([
10
- ['debug', 'log'],
11
- ['info', 'log'],
12
- ['warn', 'log']
13
- ]);
14
-
15
- class Logger {
16
-
17
- static levels = LEVELS;
18
- static defaultLevel = 'info';
19
-
20
- constructor(level = Logger.defaultLevel) {
21
- this.activeLevels = new Set();
22
- this.setLogLevel(level);
23
- }
24
-
25
- setLogLevel(level) {
26
- const levelIndex = LEVELS.indexOf(level);
27
-
28
- if (levelIndex === -1) throw new Error(`Invalid log level "${level}". Use one of these: ${LEVELS.join(', ')}`);
29
-
30
- this.activeLevels.clear();
31
-
32
- for (const [i, level] of LEVELS.entries()) {
33
- if (i >= levelIndex) this.activeLevels.add(level);
34
- }
35
- }
36
-
37
- _log(level, ...args) {
38
- console[LEVEL_TO_CONSOLE_METHOD.get(level) || level](...args);
39
- }
40
-
41
- };
42
-
43
- LEVELS.forEach(level => {
44
- if (level === 'silent') return;
45
-
46
- Logger.prototype[level] = function (...args) {
47
- if (this.activeLevels.has(level)) this._log(level, ...args);
48
- };
49
- });
50
-
51
- module.exports = Logger;
package/src/analyzer.js DELETED
@@ -1,213 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- const _ = require('lodash');
5
- const gzipSize = require('gzip-size');
6
-
7
- const Logger = require('./Logger');
8
- const Folder = require('./tree/Folder').default;
9
- const {parseBundle} = require('./parseUtils');
10
- const {createAssetsFilter} = require('./utils');
11
-
12
- const FILENAME_QUERY_REGEXP = /\?.*$/u;
13
- const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu;
14
-
15
- module.exports = {
16
- getViewerData,
17
- readStatsFromFile
18
- };
19
-
20
- function getViewerData(bundleStats, bundleDir, opts) {
21
- const {
22
- logger = new Logger(),
23
- excludeAssets = null
24
- } = opts || {};
25
-
26
- const isAssetIncluded = createAssetsFilter(excludeAssets);
27
-
28
- // Sometimes all the information is located in `children` array (e.g. problem in #10)
29
- if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
30
- const {children} = bundleStats;
31
- bundleStats = bundleStats.children[0];
32
- // Sometimes if there are additional child chunks produced add them as child assets,
33
- // leave the 1st one as that is considered the 'root' asset.
34
- for (let i = 1; i < children.length; i++) {
35
- children[i].assets.forEach((asset) => {
36
- asset.isChild = true;
37
- bundleStats.assets.push(asset);
38
- });
39
- }
40
- } else if (!_.isEmpty(bundleStats.children)) {
41
- // Sometimes if there are additional child chunks produced add them as child assets
42
- bundleStats.children.forEach((child) => {
43
- child.assets.forEach((asset) => {
44
- asset.isChild = true;
45
- bundleStats.assets.push(asset);
46
- });
47
- });
48
- }
49
-
50
- // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
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
-
57
- // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
58
- // See #22
59
- asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
60
-
61
- return FILENAME_EXTENSIONS.test(asset.name) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
62
- });
63
-
64
- // Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
65
- let bundlesSources = null;
66
- let parsedModules = null;
67
-
68
- if (bundleDir) {
69
- bundlesSources = {};
70
- parsedModules = {};
71
-
72
- for (const statAsset of bundleStats.assets) {
73
- const assetFile = path.join(bundleDir, statAsset.name);
74
- let bundleInfo;
75
-
76
- try {
77
- bundleInfo = parseBundle(assetFile);
78
- } catch (err) {
79
- const msg = (err.code === 'ENOENT') ? 'no such file' : err.message;
80
- logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`);
81
- continue;
82
- }
83
-
84
- bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
85
- Object.assign(parsedModules, bundleInfo.modules);
86
- }
87
-
88
- if (_.isEmpty(bundlesSources)) {
89
- bundlesSources = null;
90
- parsedModules = null;
91
- logger.warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n');
92
- }
93
- }
94
-
95
- const assets = bundleStats.assets.reduce((result, statAsset) => {
96
- // If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
97
- const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
98
- const modules = assetBundles ? getBundleModules(assetBundles) : [];
99
- const asset = result[statAsset.name] = _.pick(statAsset, 'size');
100
- const assetSources = bundlesSources && _.has(bundlesSources, statAsset.name) ?
101
- bundlesSources[statAsset.name] : null;
102
-
103
- if (assetSources) {
104
- asset.parsedSize = Buffer.byteLength(assetSources.src);
105
- asset.gzipSize = gzipSize.sync(assetSources.src);
106
- }
107
-
108
- // Picking modules from current bundle script
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]) {
117
- statModule.parsedSrc = parsedModules[statModule.id];
118
- } else if (isEntryModule(statModule)) {
119
- unparsedEntryModules.push(statModule);
120
- }
121
- }
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;
146
- asset.tree = createModulesTree(asset.modules);
147
- return result;
148
- }, {});
149
-
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
- }));
162
- }
163
-
164
- function readStatsFromFile(filename) {
165
- return JSON.parse(
166
- fs.readFileSync(filename, 'utf8')
167
- );
168
- }
169
-
170
- function getChildAssetBundles(bundleStats, assetName) {
171
- return (bundleStats.children || []).find((c) =>
172
- _(c.assetsByChunkName)
173
- .values()
174
- .flatten()
175
- .includes(assetName)
176
- );
177
- }
178
-
179
- function getBundleModules(bundleStats) {
180
- return _(bundleStats.chunks)
181
- .map('modules')
182
- .concat(bundleStats.modules)
183
- .compact()
184
- .flatten()
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)
188
- .value();
189
- }
190
-
191
- function assetHasModule(statAsset, statModule) {
192
- // Checking if this module is the part of asset chunks
193
- return (statModule.chunks || []).some(moduleChunk =>
194
- statAsset.chunks.includes(moduleChunk)
195
- );
196
- }
197
-
198
- function isEntryModule(statModule) {
199
- return statModule.depth === 0;
200
- }
201
-
202
- function isRuntimeModule(statModule) {
203
- return statModule.moduleType === 'runtime';
204
- }
205
-
206
- function createModulesTree(modules) {
207
- const root = new Folder('.');
208
-
209
- modules.forEach(module => root.addModule(module));
210
- root.mergeNestedFolders();
211
-
212
- return root;
213
- }
@@ -1,168 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- const {resolve, dirname} = require('path');
4
-
5
- const commander = require('commander');
6
- const {magenta} = require('chalk');
7
-
8
- const analyzer = require('../analyzer');
9
- const viewer = require('../viewer');
10
- const Logger = require('../Logger');
11
- const utils = require('../utils');
12
-
13
- const SIZES = new Set(['stat', 'parsed', 'gzip']);
14
-
15
- const program = commander
16
- .version(require('../../package.json').version)
17
- .usage(
18
- `<bundleStatsFile> [bundleDir] [options]
19
-
20
- Arguments:
21
-
22
- bundleStatsFile Path to Webpack Stats JSON file.
23
- bundleDir Directory containing all generated bundles.
24
- You should provided it if you want analyzer to show you the real parsed module sizes.
25
- By default a directory of stats file is used.`
26
- )
27
- .option(
28
- '-m, --mode <mode>',
29
- 'Analyzer mode. Should be `server`,`static` or `json`.' +
30
- br('In `server` mode analyzer will start HTTP server to show bundle report.') +
31
- br('In `static` mode single HTML file with bundle report will be generated.') +
32
- br('In `json` mode single JSON file with bundle report will be generated.'),
33
- 'server'
34
- )
35
- .option(
36
- // Had to make `host` parameter optional in order to let `-h` flag output help message
37
- // Fixes https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/239
38
- '-h, --host [host]',
39
- 'Host that will be used in `server` mode to start HTTP server.',
40
- '127.0.0.1'
41
- )
42
- .option(
43
- '-p, --port <n>',
44
- 'Port that will be used in `server` mode to start HTTP server.',
45
- 8888
46
- )
47
- .option(
48
- '-r, --report <file>',
49
- 'Path to bundle report file that will be generated in `static` mode.'
50
- )
51
- .option(
52
- '-t, --title <title>',
53
- 'String to use in title element of html report.'
54
- )
55
- .option(
56
- '-s, --default-sizes <type>',
57
- 'Module sizes to show in treemap by default.' +
58
- br(`Possible values: ${[...SIZES].join(', ')}`),
59
- 'parsed'
60
- )
61
- .option(
62
- '-O, --no-open',
63
- "Don't open report in default browser automatically."
64
- )
65
- .option(
66
- '-e, --exclude <regexp>',
67
- 'Assets that should be excluded from the report.' +
68
- br('Can be specified multiple times.'),
69
- array()
70
- )
71
- .option(
72
- '-l, --log-level <level>',
73
- 'Log level.' +
74
- br(`Possible values: ${[...Logger.levels].join(', ')}`),
75
- Logger.defaultLevel
76
- )
77
- .parse(process.argv);
78
-
79
- let {
80
- mode,
81
- host,
82
- port,
83
- report: reportFilename,
84
- title: reportTitle,
85
- defaultSizes,
86
- logLevel,
87
- open: openBrowser,
88
- exclude: excludeAssets,
89
- args: [bundleStatsFile, bundleDir]
90
- } = program;
91
- const logger = new Logger(logLevel);
92
-
93
- if (typeof reportTitle === 'undefined') {
94
- reportTitle = utils.defaultTitle;
95
- }
96
-
97
- if (!bundleStatsFile) showHelp('Provide path to Webpack Stats file as first argument');
98
- if (mode !== 'server' && mode !== 'static' && mode !== 'json') {
99
- showHelp('Invalid mode. Should be either `server`, `static` or `json`.');
100
- }
101
- if (mode === 'server') {
102
- if (!host) showHelp('Invalid host name');
103
-
104
- port = port === 'auto' ? 0 : Number(port);
105
- if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
106
- }
107
- if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
108
-
109
- bundleStatsFile = resolve(bundleStatsFile);
110
-
111
- if (!bundleDir) bundleDir = dirname(bundleStatsFile);
112
-
113
- let bundleStats;
114
- try {
115
- bundleStats = analyzer.readStatsFromFile(bundleStatsFile);
116
- } catch (err) {
117
- logger.error(`Couldn't read webpack bundle stats from "${bundleStatsFile}":\n${err}`);
118
- logger.debug(err.stack);
119
- process.exit(1);
120
- }
121
-
122
- if (mode === 'server') {
123
- viewer.startServer(bundleStats, {
124
- openBrowser,
125
- port,
126
- host,
127
- defaultSizes,
128
- reportTitle,
129
- bundleDir,
130
- excludeAssets,
131
- logger: new Logger(logLevel)
132
- });
133
- } else if (mode === 'static') {
134
- viewer.generateReport(bundleStats, {
135
- openBrowser,
136
- reportFilename: resolve(reportFilename || 'report.html'),
137
- reportTitle,
138
- defaultSizes,
139
- bundleDir,
140
- excludeAssets,
141
- logger: new Logger(logLevel)
142
- });
143
- } else if (mode === 'json') {
144
- viewer.generateJSONReport(bundleStats, {
145
- reportFilename: resolve(reportFilename || 'report.json'),
146
- bundleDir,
147
- excludeAssets,
148
- logger: new Logger(logLevel)
149
- });
150
- }
151
-
152
- function showHelp(error) {
153
- if (error) console.log(`\n ${magenta(error)}\n`);
154
- program.outputHelp();
155
- process.exit(1);
156
- }
157
-
158
- function br(str) {
159
- return `\n${' '.repeat(28)}${str}`;
160
- }
161
-
162
- function array() {
163
- const arr = [];
164
- return (val) => {
165
- arr.push(val);
166
- return arr;
167
- };
168
- }
package/src/index.js DELETED
@@ -1,6 +0,0 @@
1
- const {start} = require('./viewer');
2
-
3
- module.exports = {
4
- start,
5
- BundleAnalyzerPlugin: require('./BundleAnalyzerPlugin')
6
- };