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 <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 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 { <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 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 <type>', + '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`<!DOCTYPE html> <html> <head> @@ -59,6 +59,7 @@ function renderViewer({title, enableWebSocket, chartData, defaultSizes, mode} = <script> window.chartData = ${escapeJson(chartData)}; window.defaultSizes = ${escapeJson(defaultSizes)}; + window.compressionAlgorithm = ${escapeJson(compressionAlgorithm)}; </script> </body> </html>`; 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;