diff --git a/CHANGELOG.md b/CHANGELOG.md
index e34f5d26..9db486a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,9 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **Improvement**
* Parse bundles as ES modules based on stats JSON information ([#649](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/649) by [@eamodio](https://github.com/eamodio))
+* **New Feature**
+ * Add support for Brotli compression ([#663](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/663) by [@dcsaszar](https://github.com/dcsaszar))
+
## 4.10.2
* **Bug Fix**
@@ -71,7 +74,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
## 4.6.0
-* **New Feature**
+* **New Feature**
* Support outputting different URL in server mode ([#520](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/520) by [@southorange1228](https://github.com/southorange1228))
* Use deterministic chunk colors (#[501](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/501) by [@CreativeTechGuy](https://github.com/CreativeTechGuy))
@@ -104,19 +107,19 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **Improvement**
* Keep treemap labels visible during zooming animations for better user experience ([#414](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/414) by [@stanislawosinski](https://github.com/stanislawosinski))
-
+
* **Bug Fix**
* Don't show an empty tooltip when hovering over the FoamTree attribution group or between top-level groups ([#413](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/413) by [@stanislawosinski](https://github.com/stanislawosinski))
-
+
* **Internal**
* Upgrade FoamTree to version 3.5.0, replace vendor dependency with an NPM package ([#412](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/412) by [@stanislawosinski](https://github.com/stanislawosinski))
-
+
## 4.3.0
* **Improvement**
* Replace express with builtin node server, reducing number of dependencies ([#398](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/398) by [@TrySound](https://github.com/TrySound))
* Move `filesize` to dev dependencies, reducing number of dependencies ([#401](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/401) by [@realityking](https://github.com/realityking))
-
+
* **Internal**
* Replace Travis with GitHub actions ([#402](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/402) by [@valscion](https://github.com/valscion))
@@ -141,10 +144,10 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **Improvement**
* Support for Webpack 5
-
+
* **Bug Fix**
* Prevent crashes when `openAnalyzer` was set to true in environments where there's no program to handle opening. ([#382](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/382) by [@wbobeirne](https://github.com/wbobeirne))
-
+
* **Internal**
* Updated dependencies
* Added support for multiple Webpack versions in tests
@@ -153,7 +156,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **New Feature**
* Adds option `reportTitle` to set title in HTML reports; default remains date of report generation ([#354](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/354) by [@eoingroat](https://github.com/eoingroat))
-
+
* **Improvement**
* Added capability to parse bundles that have child assets generated ([#376](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/376) by [@masterkidan](https://github.com/masterkidan) and [#378](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/378) by [@https://github.com/dabbott](https://github.com/https://github.com/dabbott))
@@ -180,7 +183,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **Bug Fix**
* Add leading zero to hour & minute on `
` when needed ([#314](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/314) by [@mhxbe](https://github.com/mhxbe))
-
+
* **Internal**
* Update some dependencies to get rid of vulnerability warnings ([#339](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/339))
@@ -293,7 +296,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **Improvements**
* Nested folders that contain only one child folder are now visually merged i.e. `folder1 => folder2 => file1` is now shown like `folder1/folder2 => file1` (thanks to [@varun-singh-1](https://github.com/varun-singh-1) for the idea)
-
+
* **Internal**
* Dropped support for Node.js v4
* Using MobX for state management
@@ -303,10 +306,10 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
* **Improvement**
* Pretty-format the generated stats.json ([#180](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/180)) [@edmorley](https://github.com/edmorley))
-
+
* **Bug Fix**
* Properly parse Webpack 4 async chunk with `Array.concat` optimization ([#184](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/184), fixes [#183](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/183))
-
+
* **Internal**
* Refactor bundle parsing logic ([#184](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/184))
diff --git a/README.md b/README.md
index 5a45e738..2ec95ba9 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 f397d267..f5cf5d1c 100644
--- a/client/components/ModulesTreemap.jsx
+++ b/client/components/ModulesTreemap.jsx
@@ -19,11 +19,18 @@ import {store} from '../store';
import ModulesList from './ModulesList';
import Dropdown from './Dropdown';
-const SIZE_SWITCH_ITEMS = [
- {label: 'Stat', prop: 'statSize'},
- {label: 'Parsed', prop: 'parsedSize'},
- {label: 'Gzipped', prop: 'gzipSize'}
-];
+function getSizeSwitchItems() {
+ 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 {
@@ -144,7 +151,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 = getSizeSwitchItems().find(item => item.prop === sizeProp).label;
const isActive = (store.activeSize === sizeProp);
return (typeof size === 'number') ?
@@ -168,7 +175,7 @@ export default class ModulesTreemap extends Component {
};
@computed get sizeSwitchItems() {
- return store.hasParsedSizes ? SIZE_SWITCH_ITEMS : SIZE_SWITCH_ITEMS.slice(0, 1);
+ return store.hasParsedSizes ? getSizeSwitchItems() : getSizeSwitchItems().slice(0, 1);
}
@computed get activeSizeItem() {
@@ -331,7 +338,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 ec66441b..1695db11 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/src/BundleAnalyzerPlugin.js b/src/BundleAnalyzerPlugin.js
index 2018fed8..2a2020b8 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 2d0436b9..ab7c50f2 100644
--- a/src/analyzer.js
+++ b/src/analyzer.js
@@ -1,13 +1,13 @@
const fs = require('fs');
const path = require('path');
-const gzipSize = require('gzip-size');
const {parseChunked} = require('@discoveryjs/json-ext');
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|cjs)$/iu;
@@ -20,6 +20,7 @@ module.exports = {
function getViewerData(bundleStats, bundleDir, opts) {
const {
logger = new Logger(),
+ compressionAlgorithm,
excludeAssets = null
} = opts || {};
@@ -110,7 +111,8 @@ function getViewerData(bundleStats, bundleDir, opts) {
if (assetSources) {
asset.parsedSize = Buffer.byteLength(assetSources.src);
- asset.gzipSize = gzipSize.sync(assetSources.src);
+ if (compressionAlgorithm === 'gzip') asset.gzipSize = getCompressedSize('gzip', assetSources.src);
+ if (compressionAlgorithm === 'brotli') asset.brotliSize = getCompressedSize('brotli', assetSources.src);
}
// Picking modules from current bundle script
@@ -151,7 +153,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
}
asset.modules = assetModules;
- asset.tree = createModulesTree(asset.modules);
+ asset.tree = createModulesTree(asset.modules, {compressionAlgorithm});
return result;
}, {});
@@ -166,6 +168,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
statSize: asset.tree.size || asset.size,
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
+ brotliSize: asset.brotliSize,
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
}));
@@ -220,8 +223,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 c5535bdb..d66f7d0a 100755
--- a/src/bin/analyzer.js
+++ b/src/bin/analyzer.js
@@ -11,6 +11,7 @@ const Logger = require('../Logger');
const utils = require('../utils');
const SIZES = new Set(['stat', 'parsed', 'gzip']);
+const COMPRESSION_ALGORITHMS = new Set(['gzip', 'brotli']);
const program = commander
.version(require('../../package.json').version)
@@ -58,6 +59,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: ${[...COMPRESSION_ALGORITHMS].join(', ')}`),
+ 'gzip'
+ )
.option(
'-O, --no-open',
"Don't open report in default browser automatically."
@@ -84,6 +91,7 @@ let {
report: reportFilename,
title: reportTitle,
defaultSizes,
+ compressionAlgorithm,
logLevel,
open: openBrowser,
exclude: excludeAssets
@@ -104,6 +112,9 @@ if (mode === 'server') {
port = port === 'auto' ? 0 : Number(port);
if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
}
+if (!COMPRESSION_ALGORITHMS.has(compressionAlgorithm)) {
+ showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(', ')}`);
+}
if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
bundleStatsFile = resolve(bundleStatsFile);
@@ -121,6 +132,7 @@ async function parseAndAnalyse(bundleStatsFile) {
port,
host,
defaultSizes,
+ compressionAlgorithm,
reportTitle,
bundleDir,
excludeAssets,
@@ -133,6 +145,7 @@ async function parseAndAnalyse(bundleStatsFile) {
reportFilename: resolve(reportFilename || 'report.html'),
reportTitle,
defaultSizes,
+ compressionAlgorithm,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
@@ -140,6 +153,7 @@ async function parseAndAnalyse(bundleStatsFile) {
} else if (mode === 'json') {
viewer.generateJSONReport(bundleStats, {
reportFilename: resolve(reportFilename || 'report.json'),
+ compressionAlgorithm,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
@@ -159,7 +173,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..4d6bd855
--- /dev/null
+++ b/src/sizeUtils.js
@@ -0,0 +1,8 @@
+const zlib = require('zlib');
+
+export function getCompressedSize(compressionAlgorithm, input) {
+ if (compressionAlgorithm === 'gzip') return zlib.gzipSync(input, {level: 9}).length;
+ if (compressionAlgorithm === 'brotli') return zlib.brotliCompressSync(input).length;
+
+ throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`);
+}
diff --git a/src/template.js b/src/template.js
index dec70ef7..e000ec9c 100644
--- a/src/template.js
+++ b/src/template.js
@@ -39,7 +39,7 @@ function getScript(filename, mode) {
}
}
-function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSizes, mode} = {}) {
+function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSizes, compressionAlgorithm, mode} = {}) {
return html`
@@ -60,6 +60,7 @@ function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSi
window.chartData = ${escapeJson(chartData)};
window.entrypoints = ${escapeJson(entrypoints)};
window.defaultSizes = ${escapeJson(defaultSizes)};
+ window.compressionAlgorithm = ${escapeJson(compressionAlgorithm)};
`;
diff --git a/src/tree/ConcatenatedModule.js b/src/tree/ConcatenatedModule.js
index b1788ade..34ee7c05 100644
--- a/src/tree/ConcatenatedModule.js
+++ b/src/tree/ConcatenatedModule.js
@@ -5,8 +5,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();
@@ -20,6 +20,10 @@ export default class ConcatenatedModule extends Module {
return this.getGzipSize() ?? this.getEstimatedSize('gzipSize');
}
+ get brotliSize() {
+ return this.getBrotliSize() ?? this.getEstimatedSize('brotliSize');
+ }
+
getEstimatedSize(sizeType) {
const parentModuleSize = this.parent[sizeType];
@@ -53,7 +57,7 @@ export default class ConcatenatedModule extends Module {
});
const ModuleConstructor = moduleData.modules ? ConcatenatedModule : ContentModule;
- const module = new ModuleConstructor(fileName, moduleData, this);
+ const module = new ModuleConstructor(fileName, moduleData, this, this.opts);
currentFolder.addChildModule(module);
}
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 e35347f5..5d9aeb97 100644
--- a/src/tree/Folder.js
+++ b/src/tree/Folder.js
@@ -1,22 +1,36 @@
-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 (!Object.prototype.hasOwnProperty.call(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 (!Object.prototype.hasOwnProperty.call(this, key)) {
+ this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : 0;
}
- return this._gzipSize;
+ return this[key];
}
addModule(moduleData) {
@@ -41,14 +55,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);
}
@@ -56,7 +70,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 f615c7cc..079871d8 100644
--- a/src/tree/Module.js
+++ b/src/tree/Module.js
@@ -1,12 +1,12 @@
-import gzipSize from 'gzip-size';
-
import Node from './Node';
+import {getCompressedSize} from '../sizeUtils';
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() {
@@ -16,6 +16,7 @@ export default class Module extends Node {
set src(value) {
this.data.parsedSrc = value;
delete this._gzipSize;
+ delete this._brotliSize;
}
get size() {
@@ -34,16 +35,29 @@ export default class Module extends Node {
return this.getGzipSize();
}
+ get brotliSize() {
+ return this.getBrotliSize();
+ }
+
getParsedSize() {
return this.src ? this.src.length : undefined;
}
getGzipSize() {
- if (!('_gzipSize' in this)) {
- this._gzipSize = this.src ? gzipSize.sync(this.src) : undefined;
+ return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined;
+ }
+
+ getBrotliSize() {
+ return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined;
+ }
+
+ getCompressedSize(compressionAlgorithm) {
+ const key = `_${compressionAlgorithm}Size`;
+ if (!(key in this)) {
+ this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : undefined;
}
- return this._gzipSize;
+ return this[key];
}
mergeData(data) {
@@ -63,7 +77,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 3107136a..def44f4d 100644
--- a/src/viewer.js
+++ b/src/viewer.js
@@ -21,6 +21,11 @@ function resolveTitle(reportTitle) {
}
}
+function resolveDefaultSizes(defaultSizes, compressionAlgorithm) {
+ if (['gzip', 'brotli'].includes(defaultSizes)) return compressionAlgorithm;
+ return defaultSizes;
+}
+
module.exports = {
startServer,
generateReport,
@@ -38,12 +43,13 @@ async function startServer(bundleStats, opts) {
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
+ compressionAlgorithm,
excludeAssets = null,
reportTitle,
analyzerUrl
} = opts || {};
- const analyzerOpts = {logger, excludeAssets};
+ const analyzerOpts = {logger, excludeAssets, compressionAlgorithm};
let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
const entrypoints = getEntrypoints(bundleStats);
@@ -62,7 +68,8 @@ async function startServer(bundleStats, opts) {
title: resolveTitle(reportTitle),
chartData,
entrypoints,
- defaultSizes,
+ defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm),
+ compressionAlgorithm,
enableWebSocket: true
});
res.writeHead(200, {'Content-Type': 'text/html'});
@@ -136,10 +143,11 @@ async function generateReport(bundleStats, opts) {
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
+ compressionAlgorithm,
excludeAssets = null
} = opts || {};
- const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir);
+ const chartData = getChartData({logger, excludeAssets, compressionAlgorithm}, bundleStats, bundleDir);
const entrypoints = getEntrypoints(bundleStats);
if (!chartData) return;
@@ -149,7 +157,8 @@ async function generateReport(bundleStats, opts) {
title: resolveTitle(reportTitle),
chartData,
entrypoints,
- defaultSizes,
+ defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm),
+ compressionAlgorithm,
enableWebSocket: false
});
const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
@@ -165,9 +174,15 @@ 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;
diff --git a/test/analyzer.js b/test/analyzer.js
index bd2ea068..90c5f53d 100644
--- a/test/analyzer.js
+++ b/test/analyzer.js
@@ -248,6 +248,24 @@ 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');
+ });
+ });
});
});
@@ -277,6 +295,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 72f82ab9..2684fbf9 100644
--- a/test/plugin.js
+++ b/test/plugin.js
@@ -170,6 +170,26 @@ 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) {
@@ -179,8 +199,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;