diff --git a/README.md b/README.md
index 9eb77656..90159732 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ This module will help you:
4. Optimize it!
And the best thing is it supports minified bundles! It parses them to get real size of bundled modules.
-And it also shows their gzipped sizes!
+And it also shows their gzipped or Brotli sizes!
Options (for plugin)
@@ -61,7 +61,8 @@ new BundleAnalyzerPlugin(options?: object)
|**`analyzerUrl`**|`{Function}` called with `{ listenHost: string, listenHost: string, boundAddress: server.address}`. [server.address comes from Node.js](https://nodejs.org/api/net.html#serveraddress)| Default: `http://${listenHost}:${boundAddress.port}`. The URL printed to console with server mode.|
|**`reportFilename`**|`{String}`|Default: `report.html`. Path to bundle report file that will be generated in `static` mode. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
|**`reportTitle`**|`{String\|function}`|Default: function that returns pretty printed current date and time. Content of the HTML `title` element; or a function of the form `() => string` that provides the content.|
-|**`defaultSizes`**|One of: `stat`, `parsed`, `gzip`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.|
+|**`defaultSizes`**|One of: `stat`, `parsed`, `gzip`, `brotli`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.|
+|**`compressionAlgorithm`**|One of: `gzip`, `brotli`|Default: `gzip`. Compression type used to calculate the compressed module sizes.|
|**`openAnalyzer`**|`{Boolean}`|Default: `true`. Automatically open report in default browser.|
|**`generateStatsFile`**|`{Boolean}`|Default: `false`. If `true`, webpack stats JSON file will be generated in bundle output directory|
|**`statsFilename`**|`{String}`|Default: `stats.json`. Name of webpack stats JSON file that will be generated if `generateStatsFile` is `true`. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
@@ -111,23 +112,25 @@ Directory containing all generated bundles.
### `options`
```
- -V, --version output the version number
- -m, --mode Analyzer mode. Should be `server`, `static` or `json`.
- In `server` mode analyzer will start HTTP server to show bundle report.
- In `static` mode single HTML file with bundle report will be generated.
- In `json` mode single JSON file with bundle report will be generated. (default: server)
- -h, --host Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
- -p, --port Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
- -r, --report Path to bundle report file that will be generated in `static` mode. (default: report.html)
- -t, --title String to use in title element of html report. (default: pretty printed current date)
- -s, --default-sizes Module sizes to show in treemap by default.
- Possible values: stat, parsed, gzip (default: parsed)
- -O, --no-open Don't open report in default browser automatically.
- -e, --exclude Assets that should be excluded from the report.
- Can be specified multiple times.
- -l, --log-level Log level.
- Possible values: debug, info, warn, error, silent (default: info)
- -h, --help output usage information
+ -V, --version output the version number
+ -m, --mode Analyzer mode. Should be `server`, `static` or `json`.
+ In `server` mode analyzer will start HTTP server to show bundle report.
+ In `static` mode single HTML file with bundle report will be generated.
+ In `json` mode single JSON file with bundle report will be generated. (default: server)
+ -h, --host Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
+ -p, --port Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
+ -r, --report Path to bundle report file that will be generated in `static` mode. (default: report.html)
+ -t, --title String to use in title element of html report. (default: pretty printed current date)
+ -s, --default-sizes Module sizes to show in treemap by default.
+ Possible values: stat, parsed, gzip, brotli (default: parsed)
+ --compression-algorithm Compression algorithm that will be used to calculate the compressed module sizes.
+ Possible values: gzip, brotli (default: gzip)
+ -O, --no-open Don't open report in default browser automatically.
+ -e, --exclude Assets that should be excluded from the report.
+ Can be specified multiple times.
+ -l, --log-level Log level.
+ Possible values: debug, info, warn, error, silent (default: info)
+ -h, --help output usage information
```
Size definitions
@@ -151,6 +154,10 @@ as Uglify, then this value will reflect the minified size of your code.
This is the size of running the parsed bundles/modules through gzip compression.
+### `brotli`
+
+This is the size of running the parsed bundles/modules through Brotli compression.
+
Selecting Which Chunks to Display
When opened, the report displays all of the Webpack chunks for your project. It's possible to filter to a more specific list of chunks by using the sidebar or the chunk context menu.
diff --git a/client/components/ModulesTreemap.jsx b/client/components/ModulesTreemap.jsx
index 1afe3a9b..1a1b0af1 100644
--- a/client/components/ModulesTreemap.jsx
+++ b/client/components/ModulesTreemap.jsx
@@ -18,11 +18,17 @@ import Search from './Search';
import {store} from '../store';
import ModulesList from './ModulesList';
-const SIZE_SWITCH_ITEMS = [
- {label: 'Stat', prop: 'statSize'},
- {label: 'Parsed', prop: 'parsedSize'},
- {label: 'Gzipped', prop: 'gzipSize'}
-];
+function allSizeSwitchItems() {
+ const items = [
+ {label: 'Stat', prop: 'statSize'},
+ {label: 'Parsed', prop: 'parsedSize'}
+ ];
+
+ if (window.compressionAlgorithm === 'gzip') items.push({label: 'Gzipped', prop: 'gzipSize'});
+ if (window.compressionAlgorithm === 'brotli') items.push({label: 'Brotli', prop: 'brotliSize'});
+
+ return items;
+}
@observer
export default class ModulesTreemap extends Component {
@@ -138,7 +144,7 @@ export default class ModulesTreemap extends Component {
renderModuleSize(module, sizeType) {
const sizeProp = `${sizeType}Size`;
const size = module[sizeProp];
- const sizeLabel = SIZE_SWITCH_ITEMS.find(item => item.prop === sizeProp).label;
+ const sizeLabel = allSizeSwitchItems().find(item => item.prop === sizeProp).label;
const isActive = (store.activeSize === sizeProp);
return (typeof size === 'number') ?
@@ -162,7 +168,8 @@ export default class ModulesTreemap extends Component {
};
@computed get sizeSwitchItems() {
- return store.hasParsedSizes ? SIZE_SWITCH_ITEMS : SIZE_SWITCH_ITEMS.slice(0, 1);
+ const items = allSizeSwitchItems();
+ return store.hasParsedSizes ? items : items.slice(0, 1);
}
@computed get activeSizeItem() {
@@ -316,7 +323,7 @@ export default class ModulesTreemap extends Component {
{this.renderModuleSize(module, 'stat')}
{!module.inaccurateSizes && this.renderModuleSize(module, 'parsed')}
- {!module.inaccurateSizes && this.renderModuleSize(module, 'gzip')}
+ {!module.inaccurateSizes && this.renderModuleSize(module, window.compressionAlgorithm)}
{module.path &&
Path: {module.path}
}
diff --git a/client/store.js b/client/store.js
index 5710a8a0..a944a68b 100644
--- a/client/store.js
+++ b/client/store.js
@@ -4,7 +4,7 @@ import localStorage from './localStorage';
export class Store {
cid = 0;
- sizes = new Set(['statSize', 'parsedSize', 'gzipSize']);
+ sizes = new Set(['statSize', 'parsedSize', 'gzipSize', 'brotliSize']);
@observable.ref allChunks;
@observable.shallow selectedChunks;
diff --git a/package-lock.json b/package-lock.json
index 1faad240..d3daf17f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4298,11 +4298,6 @@
"domhandler": "^4.2.0"
}
},
- "duplexer": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
- "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
- },
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -5887,14 +5882,6 @@
"glogg": "^1.0.0"
}
},
- "gzip-size": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
- "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
- "requires": {
- "duplexer": "^0.1.2"
- }
- },
"handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
diff --git a/package.json b/package.json
index de192488..426f7160 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,6 @@
"acorn-walk": "^8.0.0",
"chalk": "^4.1.0",
"commander": "^7.2.0",
- "gzip-size": "^6.0.0",
"lodash": "^4.17.20",
"opener": "^1.5.2",
"sirv": "^1.0.7",
diff --git a/src/BundleAnalyzerPlugin.js b/src/BundleAnalyzerPlugin.js
index 8f493aab..b57f775d 100644
--- a/src/BundleAnalyzerPlugin.js
+++ b/src/BundleAnalyzerPlugin.js
@@ -12,6 +12,7 @@ class BundleAnalyzerPlugin {
this.opts = {
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
+ compressionAlgorithm: 'gzip',
reportFilename: null,
reportTitle: utils.defaultTitle,
defaultSizes: 'parsed',
@@ -105,6 +106,7 @@ class BundleAnalyzerPlugin {
host: this.opts.analyzerHost,
port: this.opts.analyzerPort,
reportTitle: this.opts.reportTitle,
+ compressionAlgorithm: this.opts.compressionAlgorithm,
bundleDir: this.getBundleDirFromCompiler(),
logger: this.logger,
defaultSizes: this.opts.defaultSizes,
@@ -117,6 +119,7 @@ class BundleAnalyzerPlugin {
async generateJSONReport(stats) {
await viewer.generateJSONReport(stats, {
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'),
+ compressionAlgorithm: this.opts.compressionAlgorithm,
bundleDir: this.getBundleDirFromCompiler(),
logger: this.logger,
excludeAssets: this.opts.excludeAssets
@@ -128,6 +131,7 @@ class BundleAnalyzerPlugin {
openBrowser: this.opts.openAnalyzer,
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'),
reportTitle: this.opts.reportTitle,
+ compressionAlgorithm: this.opts.compressionAlgorithm,
bundleDir: this.getBundleDirFromCompiler(),
logger: this.logger,
defaultSizes: this.opts.defaultSizes,
diff --git a/src/analyzer.js b/src/analyzer.js
index be11bbf1..6520a01e 100644
--- a/src/analyzer.js
+++ b/src/analyzer.js
@@ -2,12 +2,12 @@ const fs = require('fs');
const path = require('path');
const _ = require('lodash');
-const gzipSize = require('gzip-size');
const Logger = require('./Logger');
const Folder = require('./tree/Folder').default;
const {parseBundle} = require('./parseUtils');
const {createAssetsFilter} = require('./utils');
+const {getCompressedSize} = require('./sizeUtils');
const FILENAME_QUERY_REGEXP = /\?.*$/u;
const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu;
@@ -20,7 +20,8 @@ module.exports = {
function getViewerData(bundleStats, bundleDir, opts) {
const {
logger = new Logger(),
- excludeAssets = null
+ excludeAssets = null,
+ compressionAlgorithm
} = opts || {};
const isAssetIncluded = createAssetsFilter(excludeAssets);
@@ -102,7 +103,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
if (assetSources) {
asset.parsedSize = Buffer.byteLength(assetSources.src);
- asset.gzipSize = gzipSize.sync(assetSources.src);
+ asset[`${compressionAlgorithm}Size`] = getCompressedSize(compressionAlgorithm, assetSources.src);
}
// Picking modules from current bundle script
@@ -143,7 +144,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
}
asset.modules = assetModules;
- asset.tree = createModulesTree(asset.modules);
+ asset.tree = createModulesTree(asset.modules, {compressionAlgorithm});
return result;
}, {});
@@ -157,6 +158,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
statSize: asset.tree.size || asset.size,
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
+ brotliSize: asset.brotliSize,
groups: _.invokeMap(asset.tree.children, 'toChartData')
}));
}
@@ -203,8 +205,8 @@ function isRuntimeModule(statModule) {
return statModule.moduleType === 'runtime';
}
-function createModulesTree(modules) {
- const root = new Folder('.');
+function createModulesTree(modules, opts) {
+ const root = new Folder('.', opts);
modules.forEach(module => root.addModule(module));
root.mergeNestedFolders();
diff --git a/src/bin/analyzer.js b/src/bin/analyzer.js
index 42da2d53..a82516f4 100755
--- a/src/bin/analyzer.js
+++ b/src/bin/analyzer.js
@@ -10,7 +10,9 @@ const viewer = require('../viewer');
const Logger = require('../Logger');
const utils = require('../utils');
-const SIZES = new Set(['stat', 'parsed', 'gzip']);
+const SIZES = new Set(['stat', 'parsed', 'gzip', 'brotli']);
+
+const ALGORITHMS = new Set(['gzip', 'brotli']);
const program = commander
.version(require('../../package.json').version)
@@ -58,6 +60,12 @@ const program = commander
br(`Possible values: ${[...SIZES].join(', ')}`),
'parsed'
)
+ .option(
+ '--compression-algorithm ',
+ 'Compression algorithm that will be used to calculate the compressed module sizes.' +
+ br(`Possible values: ${[...ALGORITHMS].join(', ')}`),
+ 'gzip'
+ )
.option(
'-O, --no-open',
"Don't open report in default browser automatically."
@@ -84,6 +92,7 @@ let {
report: reportFilename,
title: reportTitle,
defaultSizes,
+ compressionAlgorithm,
logLevel,
open: openBrowser,
exclude: excludeAssets
@@ -104,7 +113,12 @@ if (mode === 'server') {
port = port === 'auto' ? 0 : Number(port);
if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
}
-if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
+if (!SIZES.has(defaultSizes)) {
+ showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
+}
+if (!ALGORITHMS.has(compressionAlgorithm)) {
+ showHelp(`Invalid compression algorithm option. Possible values are: ${[...ALGORITHMS].join(', ')}`);
+}
bundleStatsFile = resolve(bundleStatsFile);
@@ -125,6 +139,7 @@ if (mode === 'server') {
port,
host,
defaultSizes,
+ compressionAlgorithm,
reportTitle,
bundleDir,
excludeAssets,
@@ -137,6 +152,7 @@ if (mode === 'server') {
reportFilename: resolve(reportFilename || 'report.html'),
reportTitle,
defaultSizes,
+ compressionAlgorithm,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
@@ -144,6 +160,7 @@ if (mode === 'server') {
} else if (mode === 'json') {
viewer.generateJSONReport(bundleStats, {
reportFilename: resolve(reportFilename || 'report.json'),
+ compressionAlgorithm,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
@@ -157,7 +174,7 @@ function showHelp(error) {
}
function br(str) {
- return `\n${' '.repeat(28)}${str}`;
+ return `\n${' '.repeat(32)}${str}`;
}
function array() {
diff --git a/src/sizeUtils.js b/src/sizeUtils.js
new file mode 100644
index 00000000..1ce66ae4
--- /dev/null
+++ b/src/sizeUtils.js
@@ -0,0 +1,24 @@
+const zlib = require('zlib');
+
+const COMPRESSED_SIZE = {
+ gzip: gzipSize,
+ brotli: brotliSize
+};
+
+export function getCompressedSize(compressionAlgorithm, input) {
+ const fn = COMPRESSED_SIZE[compressionAlgorithm];
+ if (!fn) throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`);
+ return fn(input);
+}
+
+function gzipSize(input) {
+ return zlib.gzipSync(input, {level: 9}).length;
+}
+
+function brotliSize(input) {
+ if (typeof zlib.brotliCompressSync !== 'function') {
+ throw new Error('Brotli compression requires Node.js v10.16.0 or higher.');
+ }
+
+ return zlib.brotliCompressSync(input).length;
+}
diff --git a/src/template.js b/src/template.js
index 4ef02634..1e97eb79 100644
--- a/src/template.js
+++ b/src/template.js
@@ -39,7 +39,7 @@ function getScript(filename, mode) {
}
}
-function renderViewer({title, enableWebSocket, chartData, defaultSizes, mode} = {}) {
+function renderViewer({title, enableWebSocket, chartData, defaultSizes, compressionAlgorithm, mode} = {}) {
return html`
@@ -59,6 +59,7 @@ function renderViewer({title, enableWebSocket, chartData, defaultSizes, mode} =
`;
diff --git a/src/tree/ConcatenatedModule.js b/src/tree/ConcatenatedModule.js
index 19abe28d..f58a974c 100644
--- a/src/tree/ConcatenatedModule.js
+++ b/src/tree/ConcatenatedModule.js
@@ -7,8 +7,8 @@ import {getModulePathParts} from './utils';
export default class ConcatenatedModule extends Module {
- constructor(name, data, parent) {
- super(name, data, parent);
+ constructor(name, data, parent, opts) {
+ super(name, data, parent, opts);
this.name += ' (concatenated)';
this.children = Object.create(null);
this.fillContentModules();
diff --git a/src/tree/ContentFolder.js b/src/tree/ContentFolder.js
index 5eb647cb..c58d668e 100644
--- a/src/tree/ContentFolder.js
+++ b/src/tree/ContentFolder.js
@@ -15,6 +15,10 @@ export default class ContentFolder extends BaseFolder {
return this.getSize('gzipSize');
}
+ get brotliSize() {
+ return this.getSize('brotliSize');
+ }
+
getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];
@@ -28,6 +32,7 @@ export default class ContentFolder extends BaseFolder {
...super.toChartData(),
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
+ brotliSize: this.brotliSize,
inaccurateSizes: true
};
}
diff --git a/src/tree/ContentModule.js b/src/tree/ContentModule.js
index a33f4097..116a23c2 100644
--- a/src/tree/ContentModule.js
+++ b/src/tree/ContentModule.js
@@ -15,6 +15,10 @@ export default class ContentModule extends Module {
return this.getSize('gzipSize');
}
+ get brotliSize() {
+ return this.getSize('brotliSize');
+ }
+
getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];
diff --git a/src/tree/Folder.js b/src/tree/Folder.js
index 9bcbc006..ab64e35e 100644
--- a/src/tree/Folder.js
+++ b/src/tree/Folder.js
@@ -1,23 +1,37 @@
import _ from 'lodash';
-import gzipSize from 'gzip-size';
import Module from './Module';
import BaseFolder from './BaseFolder';
import ConcatenatedModule from './ConcatenatedModule';
import {getModulePathParts} from './utils';
+import {getCompressedSize} from '../sizeUtils';
export default class Folder extends BaseFolder {
+ constructor(name, opts) {
+ super(name);
+ this.opts = opts;
+ }
+
get parsedSize() {
return this.src ? this.src.length : 0;
}
get gzipSize() {
- if (!_.has(this, '_gzipSize')) {
- this._gzipSize = this.src ? gzipSize.sync(this.src) : 0;
+ return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined;
+ }
+
+ get brotliSize() {
+ return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined;
+ }
+
+ getCompressedSize(compressionAlgorithm) {
+ const key = `_${compressionAlgorithm}Size`;
+ if (!_.has(this, key)) {
+ this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : 0;
}
- return this._gzipSize;
+ return this[key];
}
addModule(moduleData) {
@@ -42,14 +56,14 @@ export default class Folder extends BaseFolder {
// See `test/stats/with-invalid-dynamic-require.json` as an example.
!(childNode instanceof Folder)
) {
- childNode = currentFolder.addChildFolder(new Folder(folderName));
+ childNode = currentFolder.addChildFolder(new Folder(folderName, this.opts));
}
currentFolder = childNode;
});
const ModuleConstructor = moduleData.modules ? ConcatenatedModule : Module;
- const module = new ModuleConstructor(fileName, moduleData, this);
+ const module = new ModuleConstructor(fileName, moduleData, this, this.opts);
currentFolder.addChildModule(module);
}
@@ -57,7 +71,8 @@ export default class Folder extends BaseFolder {
return {
...super.toChartData(),
parsedSize: this.parsedSize,
- gzipSize: this.gzipSize
+ gzipSize: this.gzipSize,
+ brotliSize: this.brotliSize
};
}
diff --git a/src/tree/Module.js b/src/tree/Module.js
index 5a7c87c5..95075437 100644
--- a/src/tree/Module.js
+++ b/src/tree/Module.js
@@ -1,13 +1,14 @@
import _ from 'lodash';
-import gzipSize from 'gzip-size';
+import {getCompressedSize} from '../sizeUtils';
import Node from './Node';
export default class Module extends Node {
- constructor(name, data, parent) {
+ constructor(name, data, parent, opts) {
super(name, parent);
this.data = data;
+ this.opts = opts;
}
get src() {
@@ -17,6 +18,7 @@ export default class Module extends Node {
set src(value) {
this.data.parsedSrc = value;
delete this._gzipSize;
+ delete this._brotliSize;
}
get size() {
@@ -32,11 +34,20 @@ export default class Module extends Node {
}
get gzipSize() {
- if (!_.has(this, '_gzipSize')) {
- this._gzipSize = this.src ? gzipSize.sync(this.src) : undefined;
+ return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined;
+ }
+
+ get brotliSize() {
+ return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined;
+ }
+
+ getCompressedSize(compressionAlgorithm) {
+ const key = `_${compressionAlgorithm}Size`;
+ if (!_.has(this, key)) {
+ this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : undefined;
}
- return this._gzipSize;
+ return this[key];
}
mergeData(data) {
@@ -56,7 +67,8 @@ export default class Module extends Node {
path: this.path,
statSize: this.size,
parsedSize: this.parsedSize,
- gzipSize: this.gzipSize
+ gzipSize: this.gzipSize,
+ brotliSize: this.brotliSize
};
}
diff --git a/src/viewer.js b/src/viewer.js
index 3d071140..8ec5d724 100644
--- a/src/viewer.js
+++ b/src/viewer.js
@@ -1,49 +1,55 @@
-const path = require('path');
-const fs = require('fs');
-const http = require('http');
+const path = require("path");
+const fs = require("fs");
+const http = require("http");
-const WebSocket = require('ws');
-const sirv = require('sirv');
-const _ = require('lodash');
-const {bold} = require('chalk');
+const WebSocket = require("ws");
+const sirv = require("sirv");
+const _ = require("lodash");
+const { bold } = require("chalk");
-const Logger = require('./Logger');
-const analyzer = require('./analyzer');
-const {open} = require('./utils');
-const {renderViewer} = require('./template');
+const Logger = require("./Logger");
+const analyzer = require("./analyzer");
+const { open } = require("./utils");
+const { renderViewer } = require("./template");
-const projectRoot = path.resolve(__dirname, '..');
+const projectRoot = path.resolve(__dirname, "..");
function resolveTitle(reportTitle) {
- if (typeof reportTitle === 'function') {
+ if (typeof reportTitle === "function") {
return reportTitle();
} else {
return reportTitle;
}
}
+function resolveDefaultSizes(defaultSizes, compressionAlgorithm) {
+ if (["gzip", "brotli"].includes(defaultSizes)) return compressionAlgorithm;
+ return defaultSizes;
+}
+
module.exports = {
startServer,
generateReport,
generateJSONReport,
// deprecated
- start: startServer
+ start: startServer,
};
async function startServer(bundleStats, opts) {
const {
port = 8888,
- host = '127.0.0.1',
+ host = "127.0.0.1",
openBrowser = true,
bundleDir = null,
logger = new Logger(),
- defaultSizes = 'parsed',
+ defaultSizes = "parsed",
excludeAssets = null,
reportTitle,
- analyzerUrl
+ analyzerUrl,
+ compressionAlgorithm,
} = opts || {};
- const analyzerOpts = {logger, excludeAssets};
+ const analyzerOpts = { logger, excludeAssets, compressionAlgorithm };
let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
@@ -51,38 +57,39 @@ async function startServer(bundleStats, opts) {
const sirvMiddleware = sirv(`${projectRoot}/public`, {
// disables caching and traverse the file system on every request
- dev: true
+ dev: true,
});
const server = http.createServer((req, res) => {
- if (req.method === 'GET' && req.url === '/') {
+ if (req.method === "GET" && req.url === "/") {
const html = renderViewer({
- mode: 'server',
+ mode: "server",
title: resolveTitle(reportTitle),
chartData,
- defaultSizes,
- enableWebSocket: true
+ defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm),
+ compressionAlgorithm,
+ enableWebSocket: true,
});
- res.writeHead(200, {'Content-Type': 'text/html'});
+ res.writeHead(200, { "Content-Type": "text/html" });
res.end(html);
} else {
sirvMiddleware(req, res);
}
});
- await new Promise(resolve => {
+ await new Promise((resolve) => {
server.listen(port, host, () => {
resolve();
const url = analyzerUrl({
listenPort: port,
listenHost: host,
- boundAddress: server.address()
+ boundAddress: server.address(),
});
logger.info(
- `${bold('Webpack Bundle Analyzer')} is started at ${bold(url)}\n` +
- `Use ${bold('Ctrl+C')} to close it`
+ `${bold("Webpack Bundle Analyzer")} is started at ${bold(url)}\n` +
+ `Use ${bold("Ctrl+C")} to close it`
);
if (openBrowser) {
@@ -91,10 +98,10 @@ async function startServer(bundleStats, opts) {
});
});
- const wss = new WebSocket.Server({server});
+ const wss = new WebSocket.Server({ server });
- wss.on('connection', ws => {
- ws.on('error', err => {
+ wss.on("connection", (ws) => {
+ ws.on("error", (err) => {
// Ignore network errors like `ECONNRESET`, `EPIPE`, etc.
if (err.errno) return;
@@ -105,7 +112,7 @@ async function startServer(bundleStats, opts) {
return {
ws: wss,
http: server,
- updateChartData
+ updateChartData,
};
function updateChartData(bundleStats) {
@@ -115,12 +122,14 @@ async function startServer(bundleStats, opts) {
chartData = newChartData;
- wss.clients.forEach(client => {
+ wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
- client.send(JSON.stringify({
- event: 'chartDataUpdated',
- data: newChartData
- }));
+ client.send(
+ JSON.stringify({
+ event: "chartDataUpdated",
+ data: newChartData,
+ })
+ );
}
});
}
@@ -131,29 +140,40 @@ async function generateReport(bundleStats, opts) {
openBrowser = true,
reportFilename,
reportTitle,
+ compressionAlgorithm,
bundleDir = null,
logger = new Logger(),
- defaultSizes = 'parsed',
- excludeAssets = null
+ defaultSizes = "parsed",
+ excludeAssets = null,
} = opts || {};
- const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir);
+ const chartData = getChartData(
+ { logger, excludeAssets, compressionAlgorithm },
+ bundleStats,
+ bundleDir
+ );
if (!chartData) return;
const reportHtml = renderViewer({
- mode: 'static',
+ mode: "static",
title: resolveTitle(reportTitle),
chartData,
- defaultSizes,
- enableWebSocket: false
+ defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm),
+ compressionAlgorithm,
+ enableWebSocket: false,
});
- const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
+ const reportFilepath = path.resolve(
+ bundleDir || process.cwd(),
+ reportFilename
+ );
- fs.mkdirSync(path.dirname(reportFilepath), {recursive: true});
+ fs.mkdirSync(path.dirname(reportFilepath), { recursive: true });
fs.writeFileSync(reportFilepath, reportHtml);
- logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
+ logger.info(
+ `${bold("Webpack Bundle Analyzer")} saved report to ${bold(reportFilepath)}`
+ );
if (openBrowser) {
open(`file://${reportFilepath}`, logger);
@@ -161,21 +181,35 @@ async function generateReport(bundleStats, opts) {
}
async function generateJSONReport(bundleStats, opts) {
- const {reportFilename, bundleDir = null, logger = new Logger(), excludeAssets = null} = opts || {};
+ const {
+ reportFilename,
+ bundleDir = null,
+ logger = new Logger(),
+ excludeAssets = null,
+ compressionAlgorithm,
+ } = opts || {};
- const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir);
+ const chartData = getChartData(
+ { logger, excludeAssets, compressionAlgorithm },
+ bundleStats,
+ bundleDir
+ );
if (!chartData) return;
- await fs.promises.mkdir(path.dirname(reportFilename), {recursive: true});
+ await fs.promises.mkdir(path.dirname(reportFilename), { recursive: true });
await fs.promises.writeFile(reportFilename, JSON.stringify(chartData));
- logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`);
+ logger.info(
+ `${bold("Webpack Bundle Analyzer")} saved JSON report to ${bold(
+ reportFilename
+ )}`
+ );
}
function getChartData(analyzerOpts, ...args) {
let chartData;
- const {logger} = analyzerOpts;
+ const { logger } = analyzerOpts;
try {
chartData = analyzer.getViewerData(...args, analyzerOpts);
diff --git a/test/analyzer.js b/test/analyzer.js
index 446f1e42..7b3cc19b 100644
--- a/test/analyzer.js
+++ b/test/analyzer.js
@@ -222,6 +222,23 @@ describe('Analyzer', function () {
expect(generatedReportTitle).to.match(/^webpack-bundle-analyzer \[.* at \d{2}:\d{2}\]/u);
});
});
+
+ describe('compression algorithm', function () {
+ it('should accept --compression-algorithm brotli', async function () {
+ generateReportFrom('with-modules-chunk.json', '--compression-algorithm brotli');
+ expect(await getCompressionAlgorithm()).to.equal('brotli');
+ });
+
+ it('should accept --compression-algorithm gzip', async function () {
+ generateReportFrom('with-modules-chunk.json', '--compression-algorithm gzip');
+ expect(await getCompressionAlgorithm()).to.equal('gzip');
+ });
+
+ it('should default to gzip', async function () {
+ generateReportFrom('with-modules-chunk.json');
+ expect(await getCompressionAlgorithm()).to.equal('gzip');
+ });
+ });
});
});
@@ -251,6 +268,12 @@ async function getChartData() {
return await page.evaluate(() => window.chartData);
}
+async function getCompressionAlgorithm() {
+ const page = await browser.newPage();
+ await page.goto(`file://${__dirname}/output/report.html`);
+ return await page.evaluate(() => window.compressionAlgorithm);
+}
+
function forEachChartItem(chartData, cb) {
for (const item of chartData) {
cb(item);
diff --git a/test/plugin.js b/test/plugin.js
index a34b6785..00c6c4fa 100644
--- a/test/plugin.js
+++ b/test/plugin.js
@@ -171,6 +171,46 @@ describe('Plugin', function () {
expect(error).to.equal(reportTitleError);
});
});
+
+ describe('compressionAlgorithm', function () {
+ it('should default to gzip', async function () {
+ const config = makeWebpackConfig({
+ analyzerOpts: {}
+ });
+ await webpackCompile(config, '4.44.2');
+ await expectValidReport({
+ parsedSize: 1311,
+ gzipSize: 342
+ });
+ });
+
+ it('should support gzip', async function () {
+ const config = makeWebpackConfig({
+ analyzerOpts: {
+ compressionAlgorithm: 'gzip'
+ }
+ });
+ await webpackCompile(config, '4.44.2');
+ await expectValidReport({
+ parsedSize: 1311,
+ gzipSize: 342
+ });
+ });
+
+ it('should support brotli', async function () {
+ const config = makeWebpackConfig({
+ analyzerOpts: {
+ compressionAlgorithm: 'brotli'
+ }
+ });
+ await webpackCompile(config, '4.44.2');
+ await expectValidReport({
+ parsedSize: 1311,
+ gzipSize: undefined,
+ brotliSize: 302
+ });
+ });
+ });
});
async function expectValidReport(opts) {
@@ -180,8 +220,8 @@ describe('Plugin', function () {
bundleLabel = 'bundle.js',
statSize = 141,
parsedSize = 2821,
- gzipSize = 770
- } = opts || {};
+ gzipSize
+ } = {gzipSize: 770, ...opts};
expect(fs.existsSync(`${__dirname}/output/${bundleFilename}`), 'bundle file missing').to.be.true;
expect(fs.existsSync(`${__dirname}/output/${reportFilename}`), 'report file missing').to.be.true;