From ad3efed1183bed089934dd614858a57837e9a655 Mon Sep 17 00:00:00 2001 From: ronak maheshwari Date: Fri, 11 Sep 2026 00:03:49 +0530 Subject: [PATCH 1/3] feat: Migrate to Webpack Built-in Infrastructure Logger with Backwards Compatibility --- .../use-webpack-infrastructure-logger.md | 5 + README.md | 2 +- src/BundleAnalyzerPlugin.js | 20 +- src/Logger.js | 256 ++++++++++++++++++ src/analyzer.js | 3 +- src/utils.js | 2 +- src/viewer.js | 7 +- test/Logger.js | 217 +++++++++++++++ test/plugin.js | 65 +++++ 9 files changed, 570 insertions(+), 7 deletions(-) create mode 100644 .changeset/use-webpack-infrastructure-logger.md diff --git a/.changeset/use-webpack-infrastructure-logger.md b/.changeset/use-webpack-infrastructure-logger.md new file mode 100644 index 00000000..f1c5f387 --- /dev/null +++ b/.changeset/use-webpack-infrastructure-logger.md @@ -0,0 +1,5 @@ +--- +"webpack-bundle-analyzer": minor +--- + +Use Webpack's infrastructure logger when available (`compiler.getInfrastructureLogger('webpack-bundle-analyzer')`) and deprecate the plugin's `logLevel` option in favor of Webpack's native `infrastructureLogging` configuration. diff --git a/README.md b/README.md index cdcc91a8..1d9a4db2 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ new BundleAnalyzerPlugin(options?: object) | **`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). | | **`statsOptions`** | `null` or `{Object}` | Default: `null`. Options for `stats.toJson()` method. For example you can exclude sources of your modules from stats file with `source: false` option. [See more options here](https://webpack.js.org/configuration/stats/). | | **`excludeAssets`** | `{null\|pattern\|pattern[]}` where `pattern` equals to `{String\|RegExp\|function}` | Default: `null`. Patterns that will be used to match against asset names to exclude them from the report. If pattern is a string it will be converted to RegExp via `new RegExp(str)`. If pattern is a function it should have the following signature `(assetName: string) => boolean` and should return `true` to _exclude_ matching asset. If multiple patterns are provided asset should match at least one of them to be excluded. | -| **`logLevel`** | One of: `info`, `warn`, `error`, `silent` | Default: `info`. Used to control how much details the plugin outputs. | +| **`logLevel`** | One of: `info`, `warn`, `error`, `silent` | **Deprecated**. Default: `info`. Used to control how much details the plugin outputs. Please use webpack's [`infrastructureLogging`](https://webpack.js.org/configuration/infrastructureLogging/) configuration instead. | ### Absolute output paths diff --git a/src/BundleAnalyzerPlugin.js b/src/BundleAnalyzerPlugin.js index 00229a11..770fe6f8 100644 --- a/src/BundleAnalyzerPlugin.js +++ b/src/BundleAnalyzerPlugin.js @@ -16,6 +16,7 @@ const viewer = require("./viewer"); /** @typedef {import("webpack").StatsCompilation} StatsCompilation */ /** @typedef {import("./sizeUtils").Algorithm} CompressionAlgorithm */ /** @typedef {import("./Logger").Level} LogLever */ +/** @typedef {ReturnType} WebpackLogger */ /** @typedef {import("./viewer").ViewerServerObj} ViewerServerObj */ /** @typedef {string | boolean | StatsOptions} PluginStatsOptions */ @@ -70,7 +71,7 @@ const analyzerStatsOptions = { * @property {string=} statsFilename stats filename * @property {PluginStatsOptions=} statsOptions stats options * @property {ExcludeAssets=} excludeAssets exclude assets - * @property {LogLever=} logLevel exclude assets + * @property {LogLever=} logLevel (deprecated) log level * @property {boolean=} startAnalyzer start analyzer * @property {AnalyzerUrl=} analyzerUrl start analyzer */ @@ -80,6 +81,8 @@ class BundleAnalyzerPlugin { * @param {Options=} opts options */ constructor(opts = {}) { + const hasCustomLogLevel = typeof opts.logLevel !== "undefined"; + /** @type {Required> & { analyzerPort: number, statsOptions: undefined | PluginStatsOptions }} */ this.opts = { analyzerMode: "server", @@ -102,10 +105,13 @@ class BundleAnalyzerPlugin { opts.analyzerPort === "auto" ? 0 : (opts.analyzerPort ?? 8888), }; + /** @type {boolean} */ + this.hasCustomLogLevel = hasCustomLogLevel; /** @type {Compiler | null} */ this.compiler = null; /** @type {Promise | null} */ this.server = null; + /** @type {Logger | WebpackLogger} */ this.logger = new Logger(this.opts.logLevel); } @@ -115,6 +121,18 @@ class BundleAnalyzerPlugin { apply(compiler) { this.compiler = compiler; + if (compiler.getInfrastructureLogger) { + const infraLogger = compiler.getInfrastructureLogger( + "webpack-bundle-analyzer", + ); + this.logger = Logger.createInfrastructureLoggerAdapter( + infraLogger, + this.hasCustomLogLevel ? this.opts.logLevel : undefined, + ); + } else { + this.logger = new Logger(this.opts.logLevel); + } + /** * @param {Stats} stats stats * @param {(err?: Error) => void} callback callback diff --git a/src/Logger.js b/src/Logger.js index c278a836..7ac88a8e 100644 --- a/src/Logger.js +++ b/src/Logger.js @@ -1,4 +1,5 @@ /** @typedef {import("./BundleAnalyzerPlugin").EXPECTED_ANY} EXPECTED_ANY */ +/** @typedef {ReturnType} WebpackLogger */ /** @typedef {"debug" | "info" | "warn" | "error" | "silent"} Level */ @@ -12,6 +13,221 @@ const LEVEL_TO_CONSOLE_METHOD = new Map([ ["warn", "log"], ]); +class InfrastructureLoggerAdapter { + /** + * @param {WebpackLogger} infrastructureLogger infrastructure logger + * @param {Level} userLogLevel user log level + * @param {boolean=} suppressWarning suppress deprecation warning + */ + constructor(infrastructureLogger, userLogLevel, suppressWarning = false) { + const levelIndex = LEVELS.indexOf(userLogLevel); + + if (levelIndex === -1) { + throw new Error( + `Invalid log level "${userLogLevel}". Use one of these: ${LEVELS.join(", ")}`, + ); + } + + /** @type {WebpackLogger} */ + this._logger = infrastructureLogger; + /** @type {Level} */ + this._userLogLevel = userLogLevel; + /** @type {Set} */ + this.activeLevels = new Set(); + this.setLogLevel(userLogLevel); + + if (!suppressWarning) { + this.warn( + "The 'logLevel' option is deprecated and will be removed in a future release. " + + "Please use webpack's 'infrastructureLogging.level' option instead.", + ); + } + } + + /** + * @param {Level} level level + */ + setLogLevel(level) { + const levelIndex = LEVELS.indexOf(level); + + if (levelIndex === -1) { + throw new Error( + `Invalid log level "${level}". Use one of these: ${LEVELS.join(", ")}`, + ); + } + + this.activeLevels.clear(); + + for (const [i, l] of LEVELS.entries()) { + if (i >= levelIndex) this.activeLevels.add(l); + } + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + error(...args) { + if (!this.activeLevels.has("error")) return; + this._logger.error(...args); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + warn(...args) { + if (!this.activeLevels.has("warn")) return; + this._logger.warn(...args); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + info(...args) { + if (!this.activeLevels.has("info")) return; + this._logger.info(...args); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + log(...args) { + if (!this.activeLevels.has("info")) return; + this._logger.log(...args); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + debug(...args) { + if (!this.activeLevels.has("debug")) return; + this._logger.debug(...args); + } + + trace() { + if (!this.activeLevels.has("debug")) return; + this._logger.trace(); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + group(...args) { + if (!this.activeLevels.has("info")) return; + this._logger.group(...args); + } + + groupEnd() { + if (!this.activeLevels.has("info")) return; + this._logger.groupEnd(); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + groupCollapsed(...args) { + if (!this.activeLevels.has("info")) return; + this._logger.groupCollapsed(...args); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + status(...args) { + if (!this.activeLevels.has("info")) return; + this._logger.status(...args); + } + + clear() { + if (!this.activeLevels.has("info")) return; + this._logger.clear(); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + profile(...args) { + if (!this.activeLevels.has("debug")) return; + this._logger.profile(...args); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + profileEnd(...args) { + if (!this.activeLevels.has("debug")) return; + this._logger.profileEnd(...args); + } + + /** + * @param {string | (() => string)} name name + * @returns {InfrastructureLoggerAdapter} child logger + */ + getChildLogger(name) { + return new InfrastructureLoggerAdapter( + this._logger.getChildLogger(name), + this._userLogLevel, + true, + ); + } + + /** + * @template {EXPECTED_ANY[]} T + * @param {undefined | boolean} condition condition + * @param {T} args args + */ + assert(condition, ...args) { + this._logger.assert(condition, ...args); + } + + /** + * @param {string} label label + */ + time(label) { + if (!this.activeLevels.has("info")) return; + this._logger.time(label); + } + + /** + * @param {string} label label + */ + timeLog(label) { + if (!this.activeLevels.has("info")) return; + this._logger.timeLog(label); + } + + /** + * @param {string} label label + */ + timeEnd(label) { + if (!this.activeLevels.has("info")) return; + this._logger.timeEnd(label); + } + + /** + * @param {string} label label + */ + timeAggregate(label) { + this._logger.timeAggregate(label); + } + + /** + * @param {string} label label + */ + timeAggregateEnd(label) { + this._logger.timeAggregateEnd(label); + } +} + class Logger { /** @type {Level[]} */ static levels = LEVELS; @@ -83,6 +299,33 @@ class Logger { this._log("warn", ...args); } + /** + * @template {EXPECTED_ANY[]} T + * @param {T} args args + */ + log(...args) { + if (!this.activeLevels.has("info")) return; + this._log("info", ...args); + } + + /** + * @param {string} label label + */ + time(label) { + if (!this.activeLevels.has("info")) return; + // eslint-disable-next-line no-console + console.time(label); + } + + /** + * @param {string} label label + */ + timeEnd(label) { + if (!this.activeLevels.has("info")) return; + // eslint-disable-next-line no-console + console.timeEnd(label); + } + /** * @template {EXPECTED_ANY[]} T * @param {Level} level level @@ -95,6 +338,19 @@ class Logger { (LEVEL_TO_CONSOLE_METHOD.get(level) || level) ](...args); } + + /** + * @param {WebpackLogger} infrastructureLogger infrastructure logger + * @param {Level=} userLogLevel user log level + * @returns {WebpackLogger | InfrastructureLoggerAdapter} logger adapter + */ + static createInfrastructureLoggerAdapter(infrastructureLogger, userLogLevel) { + if (typeof userLogLevel === "undefined") { + return infrastructureLogger; + } + + return new InfrastructureLoggerAdapter(infrastructureLogger, userLogLevel); + } } module.exports = Logger; diff --git a/src/analyzer.js b/src/analyzer.js index 63954c62..1d6def95 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -17,6 +17,7 @@ const FILENAME_EXTENSIONS = /\.(js|mjs|cjs|bundle)$/iu; /** @typedef {import("webpack").StatsAsset} StatsAsset */ /** @typedef {import("./BundleAnalyzerPlugin").CompressionAlgorithm} CompressionAlgorithm */ /** @typedef {import("./BundleAnalyzerPlugin").ExcludeAssets} ExcludeAssets */ +/** @typedef {ReturnType} WebpackLogger */ /** * @typedef {object} AnalyzerOptions @@ -225,7 +226,7 @@ function isEntryModule(statsModule) { /** * @typedef {object} ViewerDataOptions - * @property {Logger} logger logger + * @property {Logger | WebpackLogger} logger logger * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm * @property {ExcludeAssets} excludeAssets exclude assets */ diff --git a/src/utils.js b/src/utils.js index 4f90decf..ab3ddf7f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,7 +6,7 @@ const opener = require("opener"); /** @typedef {import("./BundleAnalyzerPlugin").ExcludeAssets} ExcludeAssets */ /** @typedef {import("./BundleAnalyzerPlugin").AnalyzerUrl} AnalyzerUrl */ -/** @typedef {import("./Logger")} Logger */ +/** @typedef {import("./Logger") | ReturnType} Logger */ const MONTHS = [ "Jan", diff --git a/src/viewer.js b/src/viewer.js index 4484b145..34f30981 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -21,6 +21,7 @@ const { open } = require("./utils"); /** @typedef {import("./BundleAnalyzerPlugin").ExcludeAssets} ExcludeAssets */ /** @typedef {import("./analyzer").ViewerDataOptions} ViewerDataOptions */ /** @typedef {import("./analyzer").ChartData} ChartData */ +/** @typedef {ReturnType} WebpackLogger */ const projectRoot = path.resolve(__dirname, ".."); @@ -107,7 +108,7 @@ function getChartData(analyzerOpts, bundleStats, bundleDir) { * @property {string} host host * @property {boolean} openBrowser true when need to open browser, otherwise false * @property {string | null} bundleDir bundle dir - * @property {Logger} logger logger + * @property {Logger | WebpackLogger} logger logger * @property {Sizes} defaultSizes default sizes * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm * @property {ExcludeAssets | null} excludeAssets exclude assets @@ -245,7 +246,7 @@ async function startServer(bundleStats, opts) { * @property {string} reportFilename report filename * @property {ReportTitle} reportTitle report title * @property {string | null} bundleDir bundle dir - * @property {Logger} logger logger + * @property {Logger | WebpackLogger} logger logger * @property {Sizes} defaultSizes default sizes * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm * @property {ExcludeAssets} excludeAssets exclude assets @@ -307,7 +308,7 @@ async function generateReport(bundleStats, opts) { * @typedef {object} GenerateJSONReportOptions * @property {string} reportFilename report filename * @property {string | null} bundleDir bundle dir - * @property {Logger} logger logger + * @property {Logger | WebpackLogger} logger logger * @property {ExcludeAssets} excludeAssets exclude assets * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm */ diff --git a/test/Logger.js b/test/Logger.js index 14811644..5e7c3952 100644 --- a/test/Logger.js +++ b/test/Logger.js @@ -95,4 +95,221 @@ describe("Logger", () => { ); }); }); + + describe("parity methods", () => { + it("should provide log(), time(), and timeEnd() methods", () => { + const logger = new Logger("info"); + const consoleLogSpy = jest + .spyOn(console, "log") + .mockImplementation(() => {}); + const consoleTimeSpy = jest + .spyOn(console, "time") + .mockImplementation(() => {}); + const consoleTimeEndSpy = jest + .spyOn(console, "timeEnd") + .mockImplementation(() => {}); + + logger.log("log message"); + expect(consoleLogSpy).toHaveBeenCalledWith("log message"); + + logger.time("timer"); + expect(consoleTimeSpy).toHaveBeenCalledWith("timer"); + + logger.timeEnd("timer"); + expect(consoleTimeEndSpy).toHaveBeenCalledWith("timer"); + + consoleLogSpy.mockRestore(); + consoleTimeSpy.mockRestore(); + consoleTimeEndSpy.mockRestore(); + }); + }); + + describe("createInfrastructureLoggerAdapter", () => { + function createMockInfraLogger() { + return { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + group: jest.fn(), + groupEnd: jest.fn(), + groupCollapsed: jest.fn(), + status: jest.fn(), + clear: jest.fn(), + profile: jest.fn(), + profileEnd: jest.fn(), + time: jest.fn(), + timeLog: jest.fn(), + timeEnd: jest.fn(), + timeAggregate: jest.fn(), + timeAggregateEnd: jest.fn(), + assert: jest.fn(), + getChildLogger: jest.fn(() => createMockInfraLogger()), + }; + } + + it("should return the infrastructure logger directly when userLogLevel is undefined", () => { + const mockInfra = createMockInfraLogger(); + const result = Logger.createInfrastructureLoggerAdapter( + mockInfra, + undefined, + ); + expect(result).toBe(mockInfra); + }); + + it("should throw if userLogLevel is invalid", () => { + const mockInfra = createMockInfraLogger(); + expect(() => + Logger.createInfrastructureLoggerAdapter(mockInfra, "invalid"), + ).toThrow(invalidLogLevelMessage("invalid")); + }); + + it("should emit deprecation warning when userLogLevel is 'info'", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "info", + ); + + expect(mockInfra.warn).toHaveBeenCalledWith( + expect.stringContaining("The 'logLevel' option is deprecated"), + ); + expect(adapter.activeLevels.has("info")).toBe(true); + expect(adapter.activeLevels.has("warn")).toBe(true); + expect(adapter.activeLevels.has("error")).toBe(true); + expect(adapter.activeLevels.has("debug")).toBe(false); + }); + + it("should not emit deprecation warning when userLogLevel is 'error' or 'silent'", () => { + const mockInfraError = createMockInfraLogger(); + Logger.createInfrastructureLoggerAdapter(mockInfraError, "error"); + expect(mockInfraError.warn).not.toHaveBeenCalled(); + + const mockInfraSilent = createMockInfraLogger(); + Logger.createInfrastructureLoggerAdapter(mockInfraSilent, "silent"); + expect(mockInfraSilent.warn).not.toHaveBeenCalled(); + }); + + it("should filter calls based on activeLevels", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "warn", + ); + mockInfra.warn.mockClear(); + + adapter.debug("debug message"); + expect(mockInfra.debug).not.toHaveBeenCalled(); + + adapter.info("info message"); + expect(mockInfra.info).not.toHaveBeenCalled(); + + adapter.log("log message"); + expect(mockInfra.log).not.toHaveBeenCalled(); + + adapter.warn("warn message"); + expect(mockInfra.warn).toHaveBeenCalledWith("warn message"); + + adapter.error("error message"); + expect(mockInfra.error).toHaveBeenCalledWith("error message"); + }); + + it("should forward trace, profile, and profileEnd when debug is active", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "debug", + ); + + adapter.trace(); + expect(mockInfra.trace).toHaveBeenCalled(); + + adapter.profile("label"); + expect(mockInfra.profile).toHaveBeenCalledWith("label"); + + adapter.profileEnd("label"); + expect(mockInfra.profileEnd).toHaveBeenCalledWith("label"); + }); + + it("should forward time, timeLog, timeEnd, status, clear, and groups when info is active", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "info", + ); + + adapter.time("t"); + expect(mockInfra.time).toHaveBeenCalledWith("t"); + + adapter.timeLog("t"); + expect(mockInfra.timeLog).toHaveBeenCalledWith("t"); + + adapter.timeEnd("t"); + expect(mockInfra.timeEnd).toHaveBeenCalledWith("t"); + + adapter.status("status"); + expect(mockInfra.status).toHaveBeenCalledWith("status"); + + adapter.clear(); + expect(mockInfra.clear).toHaveBeenCalled(); + + adapter.group("g"); + expect(mockInfra.group).toHaveBeenCalledWith("g"); + + adapter.groupCollapsed("gc"); + expect(mockInfra.groupCollapsed).toHaveBeenCalledWith("gc"); + + adapter.groupEnd(); + expect(mockInfra.groupEnd).toHaveBeenCalled(); + }); + + it("should forward assert and timeAggregate regardless of log level", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "silent", + ); + + const assertion = false; + adapter.assert(assertion, "assert fail"); + expect(mockInfra.assert).toHaveBeenCalledWith(assertion, "assert fail"); + + adapter.timeAggregate("tagg"); + expect(mockInfra.timeAggregate).toHaveBeenCalledWith("tagg"); + + adapter.timeAggregateEnd("tagg"); + expect(mockInfra.timeAggregateEnd).toHaveBeenCalledWith("tagg"); + }); + + it("should support setLogLevel and throw on invalid level", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "error", + ); + + expect(adapter.activeLevels.has("warn")).toBe(false); + adapter.setLogLevel("warn"); + expect(adapter.activeLevels.has("warn")).toBe(true); + + expect(() => adapter.setLogLevel("invalid")).toThrow( + invalidLogLevelMessage("invalid"), + ); + }); + + it("should wrap child logger in getChildLogger", () => { + const mockInfra = createMockInfraLogger(); + const adapter = Logger.createInfrastructureLoggerAdapter( + mockInfra, + "warn", + ); + + const childAdapter = adapter.getChildLogger("child"); + expect(mockInfra.getChildLogger).toHaveBeenCalledWith("child"); + expect(childAdapter.activeLevels.has("warn")).toBe(true); + expect(childAdapter.activeLevels.has("info")).toBe(false); + }); + }); }); diff --git a/test/plugin.js b/test/plugin.js index 1a7547ef..2764a70f 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -3,6 +3,7 @@ const path = require("node:path"); const url = require("node:url"); const puppeteer = require("puppeteer"); const BundleAnalyzerPlugin = require("../src/BundleAnalyzerPlugin"); +const Logger = require("../src/Logger"); const { isZstdSupported } = require("../src/sizeUtils"); const { forEachWebpackVersion, @@ -302,6 +303,70 @@ describe("Plugin", () => { }); }); } + + describe("logLevel", () => { + it("should use compiler.getInfrastructureLogger directly when logLevel is not provided", () => { + const plugin = new BundleAnalyzerPlugin(); + const mockInfraLogger = { + warn: jest.fn(), + info: jest.fn(), + error: jest.fn(), + }; + const mockCompiler = { + getInfrastructureLogger: jest.fn(() => mockInfraLogger), + hooks: { + done: { tapAsync: jest.fn() }, + }, + }; + + plugin.apply(mockCompiler); + + expect(mockCompiler.getInfrastructureLogger).toHaveBeenCalledWith( + "webpack-bundle-analyzer", + ); + expect(plugin.logger).toBe(mockInfraLogger); + }); + + it("should wrap infrastructure logger and emit deprecation warning when logLevel is provided", () => { + const plugin = new BundleAnalyzerPlugin({ logLevel: "info" }); + const mockInfraLogger = { + warn: jest.fn(), + info: jest.fn(), + error: jest.fn(), + }; + const mockCompiler = { + getInfrastructureLogger: jest.fn(() => mockInfraLogger), + hooks: { + done: { tapAsync: jest.fn() }, + }, + }; + + plugin.apply(mockCompiler); + + expect(mockCompiler.getInfrastructureLogger).toHaveBeenCalledWith( + "webpack-bundle-analyzer", + ); + expect(mockInfraLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("The 'logLevel' option is deprecated"), + ); + expect(plugin.logger).not.toBe(mockInfraLogger); + }); + + it("should fall back to Logger when compiler.getInfrastructureLogger is undefined", () => { + const plugin = new BundleAnalyzerPlugin({ logLevel: "warn" }); + const mockCompiler = { + hooks: { + done: { tapAsync: jest.fn() }, + }, + }; + + plugin.apply(mockCompiler); + + expect(plugin.logger).toBeInstanceOf(Logger); + expect(plugin.logger.activeLevels.has("warn")).toBe(true); + expect(plugin.logger.activeLevels.has("info")).toBe(false); + }); + }); }); }); }); From 84293309766cfc9a47957170134cf3eab35be085 Mon Sep 17 00:00:00 2001 From: ronak maheshwari Date: Sat, 12 Sep 2026 08:58:32 +0530 Subject: [PATCH 2/3] refactor: simplify infrastructure logger adapter with Proxy and improve test coverage --- src/Logger.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/Logger.js | 34 +++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/Logger.js b/src/Logger.js index 7ac88a8e..40e5dffa 100644 --- a/src/Logger.js +++ b/src/Logger.js @@ -343,13 +343,95 @@ class Logger { * @param {WebpackLogger} infrastructureLogger infrastructure logger * @param {Level=} userLogLevel user log level * @returns {WebpackLogger | InfrastructureLoggerAdapter} logger adapter + * @param {boolean=} warned whether deprecation warning has been logged + * @returns {WebpackLogger} logger adapter */ static createInfrastructureLoggerAdapter(infrastructureLogger, userLogLevel) { + static createInfrastructureLoggerAdapter( + infrastructureLogger, + userLogLevel, + warned = false, + ) { if (typeof userLogLevel === "undefined") { return infrastructureLogger; } return new InfrastructureLoggerAdapter(infrastructureLogger, userLogLevel); + const levelIndex = LEVELS.indexOf(userLogLevel); + + if (levelIndex === -1) { + throw new Error( + `Invalid log level "${userLogLevel}". Use one of these: ${LEVELS.join(", ")}`, + ); + } + + /** @type {Set} */ + const activeLevels = new Set(); + + for (const [i, level] of LEVELS.entries()) { + if (i >= levelIndex) activeLevels.add(level); + } + + if (!warned && activeLevels.has("warn")) { + infrastructureLogger.warn( + "The 'logLevel' option is deprecated and will be removed in a future release. " + + "Please use webpack's 'infrastructureLogging.level' option instead.", + ); + } + + return new Proxy(infrastructureLogger, { + get(target, prop, receiver) { + if (prop === "activeLevels") { + return activeLevels; + } + + if (prop === "setLogLevel") { + return (/** @type {Level} */ level) => { + const idx = LEVELS.indexOf(level); + + if (idx === -1) { + throw new Error( + `Invalid log level "${level}". Use one of these: ${LEVELS.join(", ")}`, + ); + } + + activeLevels.clear(); + + for (const [i, l] of LEVELS.entries()) { + if (i >= idx) activeLevels.add(l); + } + }; + } + + if (prop === "getChildLogger") { + return (/** @type {string | (() => string)} */ name) => + Logger.createInfrastructureLoggerAdapter( + target.getChildLogger(name), + userLogLevel, + true, + ); + } + + const value = Reflect.get(target, prop, receiver); + + if (typeof value === "function") { + const isManagedLevel = + LEVELS.includes(/** @type {Level} */ (prop)) || prop === "log"; + const levelToCheck = prop === "log" ? "info" : prop; + + if ( + isManagedLevel && + !activeLevels.has(/** @type {Level} */ (levelToCheck)) + ) { + return () => {}; + } + + return value.bind(target); + } + + return value; + }, + }); } } diff --git a/test/Logger.js b/test/Logger.js index 5e7c3952..1194748c 100644 --- a/test/Logger.js +++ b/test/Logger.js @@ -99,11 +99,15 @@ describe("Logger", () => { describe("parity methods", () => { it("should provide log(), time(), and timeEnd() methods", () => { const logger = new Logger("info"); + describe("console output", () => { + it("should log to console using corresponding console methods", () => { const consoleLogSpy = jest .spyOn(console, "log") .mockImplementation(() => {}); const consoleTimeSpy = jest .spyOn(console, "time") + const consoleErrorSpy = jest + .spyOn(console, "error") .mockImplementation(() => {}); const consoleTimeEndSpy = jest .spyOn(console, "timeEnd") @@ -111,16 +115,27 @@ describe("Logger", () => { logger.log("log message"); expect(consoleLogSpy).toHaveBeenCalledWith("log message"); + const l = new Logger("debug"); + l.debug("debug message"); + expect(consoleLogSpy).toHaveBeenCalledWith("debug message"); logger.time("timer"); expect(consoleTimeSpy).toHaveBeenCalledWith("timer"); + l.info("info message"); + expect(consoleLogSpy).toHaveBeenCalledWith("info message"); logger.timeEnd("timer"); expect(consoleTimeEndSpy).toHaveBeenCalledWith("timer"); + l.warn("warn message"); + expect(consoleLogSpy).toHaveBeenCalledWith("warn message"); + + l.error("error message"); + expect(consoleErrorSpy).toHaveBeenCalledWith("error message"); consoleLogSpy.mockRestore(); consoleTimeSpy.mockRestore(); consoleTimeEndSpy.mockRestore(); + consoleErrorSpy.mockRestore(); }); }); @@ -146,6 +161,7 @@ describe("Logger", () => { timeAggregate: jest.fn(), timeAggregateEnd: jest.fn(), assert: jest.fn(), + customProp: "value", getChildLogger: jest.fn(() => createMockInfraLogger()), }; } @@ -167,6 +183,7 @@ describe("Logger", () => { }); it("should emit deprecation warning when userLogLevel is 'info'", () => { + it("should emit deprecation warning when userLogLevel has warn active", () => { const mockInfra = createMockInfraLogger(); const adapter = Logger.createInfrastructureLoggerAdapter( mockInfra, @@ -217,27 +234,35 @@ describe("Logger", () => { }); it("should forward trace, profile, and profileEnd when debug is active", () => { + it("should allow debug and log calls when their levels are active", () => { const mockInfra = createMockInfraLogger(); const adapter = Logger.createInfrastructureLoggerAdapter( mockInfra, "debug", ); + mockInfra.warn.mockClear(); adapter.trace(); expect(mockInfra.trace).toHaveBeenCalled(); + adapter.debug("debug message"); + expect(mockInfra.debug).toHaveBeenCalledWith("debug message"); adapter.profile("label"); expect(mockInfra.profile).toHaveBeenCalledWith("label"); adapter.profileEnd("label"); expect(mockInfra.profileEnd).toHaveBeenCalledWith("label"); + adapter.log("log message"); + expect(mockInfra.log).toHaveBeenCalledWith("log message"); }); it("should forward time, timeLog, timeEnd, status, clear, and groups when info is active", () => { + it("should suppress error call when level is silent", () => { const mockInfra = createMockInfraLogger(); const adapter = Logger.createInfrastructureLoggerAdapter( mockInfra, "info", + "silent", ); adapter.time("t"); @@ -263,9 +288,12 @@ describe("Logger", () => { adapter.groupEnd(); expect(mockInfra.groupEnd).toHaveBeenCalled(); + adapter.error("error message"); + expect(mockInfra.error).not.toHaveBeenCalled(); }); it("should forward assert and timeAggregate regardless of log level", () => { + it("should forward arbitrary non-level methods and access non-function properties", () => { const mockInfra = createMockInfraLogger(); const adapter = Logger.createInfrastructureLoggerAdapter( mockInfra, @@ -275,12 +303,15 @@ describe("Logger", () => { const assertion = false; adapter.assert(assertion, "assert fail"); expect(mockInfra.assert).toHaveBeenCalledWith(assertion, "assert fail"); + adapter.time("timer"); + expect(mockInfra.time).toHaveBeenCalledWith("timer"); adapter.timeAggregate("tagg"); expect(mockInfra.timeAggregate).toHaveBeenCalledWith("tagg"); adapter.timeAggregateEnd("tagg"); expect(mockInfra.timeAggregateEnd).toHaveBeenCalledWith("tagg"); + expect(adapter.customProp).toBe("value"); }); it("should support setLogLevel and throw on invalid level", () => { @@ -300,14 +331,17 @@ describe("Logger", () => { }); it("should wrap child logger in getChildLogger", () => { + it("should wrap child logger in getChildLogger without re-warning", () => { const mockInfra = createMockInfraLogger(); const adapter = Logger.createInfrastructureLoggerAdapter( mockInfra, "warn", ); + mockInfra.warn.mockClear(); const childAdapter = adapter.getChildLogger("child"); expect(mockInfra.getChildLogger).toHaveBeenCalledWith("child"); + expect(mockInfra.warn).not.toHaveBeenCalled(); expect(childAdapter.activeLevels.has("warn")).toBe(true); expect(childAdapter.activeLevels.has("info")).toBe(false); }); From 242e5dd782a340cb8345ea44e4dd7b24ccb3b1b9 Mon Sep 17 00:00:00 2001 From: ronak maheshwari Date: Sat, 12 Sep 2026 09:18:50 +0530 Subject: [PATCH 3/3] feat:Cover server analyzer mode --- test/plugin.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/plugin.js b/test/plugin.js index 2764a70f..8d61d0db 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -130,6 +130,40 @@ describe("Plugin", () => { expect(chartData).toBeDefined(); }); + it("should start a server without opening a browser", async () => { + const analyzerUrl = jest.fn(() => "http://analyzer.test"); + const config = makeWebpackConfig({ + analyzerOpts: { + analyzerMode: "server", + analyzerPort: "auto", + analyzerUrl, + openAnalyzer: false, + }, + }); + const [plugin] = config.plugins; + + try { + await webpackCompile(config); + + const server = await plugin.server; + expect(server.http.listening).toBe(true); + expect(analyzerUrl).toHaveBeenCalledWith( + expect.objectContaining({ + listenHost: "127.0.0.1", + listenPort: 0, + }), + ); + } finally { + if (plugin.server) { + const server = await plugin.server; + server.ws.close(); + await new Promise((resolve) => { + server.http.close(() => resolve()); + }); + } + } + }); + it("should use each compiler output path when a plugin instance is reused", async () => { const plugin = new BundleAnalyzerPlugin({ analyzerMode: "json",