Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@
"watch:analyzer": "npm run build:analyzer -- --watch",
"watch:viewer": "npm run build:viewer -- --node-env=development --watch",
"npm-publish": "npm run lint && npm run build && npm test && npm publish",
"lint": "npm run lint:code && npm run fmt:check",
"lint": "npm run lint:code && npm run lint:types && npm run fmt:check",
"lint:code": "eslint --cache .",
"lint:types": "tsc --pretty --noEmit",
"fmt": "npm run fmt:base -- --log-level warn --write",
"fmt:check": "npm run fmt:base -- --check",
"fmt:base": "prettier --cache --ignore-unknown .",
Expand Down Expand Up @@ -71,6 +72,8 @@
"@babel/preset-react": "^7.26.3",
"@babel/runtime": "^7.26.9",
"@carrotsearch/foamtree": "^3.5.0",
"@types/html-escaper": "^3.0.4",
"@types/opener": "^1.4.3",
"autoprefixer": "^10.2.5",
"babel-eslint": "^10.1.0",
"babel-loader": "^10.0.0",
Expand All @@ -95,6 +98,7 @@
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.1.2",
"tinyglobby": "^0.2.15",
"typescript": "^5.9.3",
"webpack": "^5.105.2",
"webpack-4": "npm:webpack@^4",
"webpack-cli": "^6.0.1",
Expand Down
107 changes: 92 additions & 15 deletions src/BundleAnalyzerPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,54 @@ 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",
Expand All @@ -19,31 +65,38 @@ class BundleAnalyzerPlugin {
openAnalyzer: true,
generateStatsFile: false,
statsFilename: "stats.json",
statsOptions: null,
statsOptions: undefined,
excludeAssets: null,
logLevel: "info",
// deprecated
// TODO deprecated
startAnalyzer: true,
analyzerUrl: utils.defaultAnalyzerUrl,
...opts,
analyzerPort:
"analyzerPort" in opts
? opts.analyzerPort === "auto"
? 0
: opts.analyzerPort
: 8888,
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) {
Expand Down Expand Up @@ -72,7 +125,7 @@ class BundleAnalyzerPlugin {
await Promise.all(actions.map((action) => action()));
callback();
} catch (err) {
callback(err);
callback(/** @type {Error} */ (err));
}
});
} else {
Expand All @@ -83,13 +136,19 @@ class BundleAnalyzerPlugin {
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(
this.compiler.outputPath,
/** @type {Compiler} */
(this.compiler).outputPath,
this.opts.statsFilename,
);
await fs.promises.mkdir(path.dirname(statsFilepath), { recursive: true });
Expand All @@ -107,6 +166,10 @@ class BundleAnalyzerPlugin {
}
}

/**
* @param {StatsCompilation} stats stats
* @returns {Promise<void>}
*/
async startAnalyzerServer(stats) {
if (this.server) {
(await this.server).updateChartData(stats);
Expand All @@ -126,10 +189,15 @@ class BundleAnalyzerPlugin {
}
}

/**
* @param {StatsCompilation} stats stats
* @returns {Promise<void>}
*/
async generateJSONReport(stats) {
await viewer.generateJSONReport(stats, {
reportFilename: path.resolve(
this.compiler.outputPath,
/** @type {Compiler} */
(this.compiler).outputPath,
this.opts.reportFilename || "report.json",
),
compressionAlgorithm: this.opts.compressionAlgorithm,
Expand All @@ -139,11 +207,16 @@ class BundleAnalyzerPlugin {
});
}

/**
* @param {StatsCompilation} stats stats
* @returns {Promise<void>}
*/
async generateStaticReport(stats) {
await viewer.generateReport(stats, {
openBrowser: this.opts.openAnalyzer,
reportFilename: path.resolve(
this.compiler.outputPath,
/** @type {Compiler} */
(this.compiler).outputPath,
this.opts.reportFilename || "report.html",
),
reportTitle: this.opts.reportTitle,
Expand All @@ -156,18 +229,22 @@ class BundleAnalyzerPlugin {
}

getBundleDirFromCompiler() {
if (typeof this.compiler.outputFileSystem.constructor === "undefined") {
return this.compiler.outputPath;
const outputFileSystemConstructor =
/** @type {OutputFileSystem} */
(/** @type {Compiler} */ (this.compiler).outputFileSystem).constructor;

if (typeof outputFileSystemConstructor === "undefined") {
return /** @type {Compiler} */ (this.compiler).outputPath;
}
switch (this.compiler.outputFileSystem.constructor.name) {
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 this.compiler.outputPath;
return /** @type {Compiler} */ (this.compiler).outputPath;
}
}
}
Expand Down
69 changes: 60 additions & 9 deletions src/Logger.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
/** @typedef {import("./BundleAnalyzerPlugin").EXPECTED_ANY} EXPECTED_ANY */

/** @typedef {"debug" | "info" | "warn" | "error" | "silent"} Level */

/** @type {Level[]} */
const LEVELS = ["debug", "info", "warn", "error", "silent"];

/** @type {Map<Level, string>} */
const LEVEL_TO_CONSOLE_METHOD = new Map([
["debug", "log"],
["info", "log"],
["warn", "log"],
]);

class Logger {
/** @type {Level[]} */
static levels = LEVELS;

/** @type {Level} */
static defaultLevel = "info";

/**
* @param {Level=} level level
*/
constructor(level = Logger.defaultLevel) {
/** @type {Set<Level>} */
this.activeLevels = new Set();
this.setLogLevel(level);
}

/**
* @param {Level} level level
*/
setLogLevel(level) {
const levelIndex = LEVELS.indexOf(level);

Expand All @@ -32,18 +47,54 @@ class Logger {
}
}

_log(level, ...args) {
// eslint-disable-next-line no-console
console[LEVEL_TO_CONSOLE_METHOD.get(level) || level](...args);
/**
* @template {EXPECTED_ANY[]} T
* @param {T} args args
*/
debug(...args) {
if (!this.activeLevels.has("debug")) return;
this._log("debug", ...args);
}
}

for (const level of LEVELS) {
if (level === "silent") continue;
/**
* @template {EXPECTED_ANY[]} T
* @param {T} args args
*/
info(...args) {
if (!this.activeLevels.has("info")) return;
this._log("info", ...args);
}

Logger.prototype[level] = function log(...args) {
if (this.activeLevels.has(level)) this._log(level, ...args);
};
/**
* @template {EXPECTED_ANY[]} T
* @param {T} args args
*/
error(...args) {
if (!this.activeLevels.has("error")) return;
this._log("error", ...args);
}

/**
* @template {EXPECTED_ANY[]} T
* @param {T} args args
*/
warn(...args) {
if (!this.activeLevels.has("warn")) return;
this._log("warn", ...args);
}

/**
* @template {EXPECTED_ANY[]} T
* @param {Level} level level
* @param {T} args args
*/
_log(level, ...args) {
// eslint-disable-next-line no-console
console[
/** @type {Exclude<Level, "silent">} */
(LEVEL_TO_CONSOLE_METHOD.get(level) || level)
](...args);
}
}

module.exports = Logger;
Loading
Loading