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! <h2 align="center">Options (for plugin)</h2> @@ -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 <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> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1) - -p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888) - -r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html) - -t, --title <title> String to use in title element of html report. (default: pretty printed current date) - -s, --default-sizes <type> 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 <regexp> Assets that should be excluded from the report. - Can be specified multiple times. - -l, --log-level <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 <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> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1) + -p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888) + -r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html) + -t, --title <title> String to use in title element of html report. (default: pretty printed current date) + -s, --default-sizes <type> Module sizes to show in treemap by default. + Possible values: stat, parsed, gzip, brotli (default: parsed) + --compression-algorithm <type> 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 <regexp> Assets that should be excluded from the report. + Can be specified multiple times. + -l, --log-level <level> Log level. + Possible values: debug, info, warn, error, silent (default: info) + -h, --help output usage information ``` <h2 align="center" id="size-definitions">Size definitions</h2> @@ -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. + <h2 align="center">Selecting Which Chunks to Display</h2> 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 { <br/> {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 && <div>Path: <strong>{module.path}</strong></div> } 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 <type>', + '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`<!DOCTYPE html> <html> <head> @@ -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)}; </script> </body> </html>`; 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;