UNPKG

webpack-bundle-analyzer

Version:

Webpack plugin and CLI utility that represents bundle content as convenient interactive zoomable treemap

233 lines (217 loc) 8.23 kB
"use strict"; const fs = require("node:fs"); const path = require("node:path"); const { bold } = require("picocolors"); const Logger = require("./Logger"); const { writeStats } = require("./statsUtils"); const utils = require("./utils"); const viewer = require("./viewer"); /** @typedef {import("net").AddressInfo} AddressInfo */ /** @typedef {import("webpack").Compiler} Compiler */ /** @typedef {import("webpack").OutputFileSystem} OutputFileSystem */ /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").StatsOptions} StatsOptions */ /** @typedef {import("webpack").StatsAsset} StatsAsset */ /** @typedef {import("webpack").StatsCompilation} StatsCompilation */ /** @typedef {import("./sizeUtils").Algorithm} CompressionAlgorithm */ /** @typedef {import("./Logger").Level} LogLever */ /** @typedef {import("./viewer").ViewerServerObj} ViewerServerObj */ /** @typedef {string | boolean | StatsOptions} PluginStatsOptions */ // eslint-disable-next-line jsdoc/reject-any-type /** @typedef {any} EXPECTED_ANY */ /** @typedef {"static" | "json" | "server" | "disabled"} Mode */ /** @typedef {string | RegExp | ((asset: string) => void)} Pattern */ /** @typedef {null | Pattern | Pattern[]} ExcludeAssets */ /** @typedef {"stat" | "parsed" | "gzip" | "brotli" | "zstd"} Sizes */ /** @typedef {string | (() => string)} ReportTitle */ /** @typedef {(options: { listenHost: string, listenPort: number, boundAddress: string | AddressInfo | null }) => string} AnalyzerUrl */ /** * @typedef {object} Options * @property {Mode=} analyzerMode analyzer mode * @property {string=} analyzerHost analyzer host * @property {"auto" | number=} analyzerPort analyzer port * @property {CompressionAlgorithm=} compressionAlgorithm compression algorithm * @property {string | null=} reportFilename report filename * @property {ReportTitle=} reportTitle report title * @property {Sizes=} defaultSizes default sizes * @property {boolean=} openAnalyzer open analyzer * @property {boolean=} generateStatsFile generate stats file * @property {string=} statsFilename stats filename * @property {PluginStatsOptions=} statsOptions stats options * @property {ExcludeAssets=} excludeAssets exclude assets * @property {LogLever=} logLevel exclude assets * @property {boolean=} startAnalyzer start analyzer * @property {AnalyzerUrl=} analyzerUrl start analyzer */ class BundleAnalyzerPlugin { /** * @param {Options=} opts options */ constructor(opts = {}) { /** @type {Required<Omit<Options, "analyzerPort" | "statsOptions">> & { analyzerPort: number, statsOptions: undefined | PluginStatsOptions }} */ this.opts = { analyzerMode: "server", analyzerHost: "127.0.0.1", compressionAlgorithm: "gzip", reportFilename: null, reportTitle: utils.defaultTitle, defaultSizes: "parsed", openAnalyzer: true, generateStatsFile: false, statsFilename: "stats.json", statsOptions: undefined, excludeAssets: null, logLevel: "info", // TODO deprecated startAnalyzer: true, analyzerUrl: utils.defaultAnalyzerUrl, ...opts, analyzerPort: opts.analyzerPort === "auto" ? 0 : opts.analyzerPort ?? 8888 }; /** @type {Compiler | null} */ this.compiler = null; /** @type {Promise<ViewerServerObj> | null} */ this.server = null; this.logger = new Logger(this.opts.logLevel); } /** * @param {Compiler} compiler compiler */ apply(compiler) { this.compiler = compiler; /** * @param {Stats} stats stats * @param {(err?: Error) => void} callback callback */ const done = (stats, callback) => { callback ||= () => {}; /** @type {(() => Promise<void>)[]} */ const actions = []; if (this.opts.generateStatsFile) { actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions))); } // Handling deprecated `startAnalyzer` flag if (this.opts.analyzerMode === "server" && !this.opts.startAnalyzer) { this.opts.analyzerMode = "disabled"; } if (this.opts.analyzerMode === "server") { actions.push(() => this.startAnalyzerServer(stats.toJson())); } else if (this.opts.analyzerMode === "static") { actions.push(() => this.generateStaticReport(stats.toJson())); } else if (this.opts.analyzerMode === "json") { actions.push(() => this.generateJSONReport(stats.toJson())); } if (actions.length) { // Making analyzer logs to be after all webpack logs in the console setImmediate(async () => { try { await Promise.all(actions.map(action => action())); callback(); } catch (err) { callback(/** @type {Error} */err); } }); } else { callback(); } }; if (compiler.hooks) { compiler.hooks.done.tapAsync("webpack-bundle-analyzer", done); } else { // @ts-expect-error old webpack@4 API compiler.plugin("done", done); } } /** * @param {StatsCompilation} stats stats * @returns {Promise<void>} */ async generateStatsFile(stats) { const statsFilepath = path.resolve(/** @type {Compiler} */ this.compiler.outputPath, this.opts.statsFilename); await fs.promises.mkdir(path.dirname(statsFilepath), { recursive: true }); try { await writeStats(stats, statsFilepath); this.logger.info(`${bold("Webpack Bundle Analyzer")} saved stats file to ${bold(statsFilepath)}`); } catch (error) { this.logger.error(`${bold("Webpack Bundle Analyzer")} error saving stats file to ${bold(statsFilepath)}: ${error}`); } } /** * @param {StatsCompilation} stats stats * @returns {Promise<void>} */ async startAnalyzerServer(stats) { if (this.server) { (await this.server).updateChartData(stats); } else { this.server = viewer.startServer(stats, { openBrowser: this.opts.openAnalyzer, 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, excludeAssets: this.opts.excludeAssets, analyzerUrl: this.opts.analyzerUrl }); } } /** * @param {StatsCompilation} stats stats * @returns {Promise<void>} */ async generateJSONReport(stats) { await viewer.generateJSONReport(stats, { reportFilename: path.resolve(/** @type {Compiler} */ this.compiler.outputPath, this.opts.reportFilename || "report.json"), compressionAlgorithm: this.opts.compressionAlgorithm, bundleDir: this.getBundleDirFromCompiler(), logger: this.logger, excludeAssets: this.opts.excludeAssets }); } /** * @param {StatsCompilation} stats stats * @returns {Promise<void>} */ async generateStaticReport(stats) { await viewer.generateReport(stats, { openBrowser: this.opts.openAnalyzer, reportFilename: path.resolve(/** @type {Compiler} */ 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, excludeAssets: this.opts.excludeAssets }); } getBundleDirFromCompiler() { const outputFileSystemConstructor = /** @type {OutputFileSystem} */ (/** @type {Compiler} */this.compiler.outputFileSystem).constructor; if (typeof outputFileSystemConstructor === "undefined") { return /** @type {Compiler} */this.compiler.outputPath; } switch (outputFileSystemConstructor.name) { case "MemoryFileSystem": return null; // Detect AsyncMFS used by Nuxt 2.5 that replaces webpack's MFS during development // Related: #274 case "AsyncMFS": return null; default: return /** @type {Compiler} */this.compiler.outputPath; } } } module.exports = BundleAnalyzerPlugin;