Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion client/components/ContextMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default class ContextMenu extends PureComponent {
/**
* Handle document-wide `mousedown` events to detect clicks
* outside the context menu.
* @param {MouseEvent} event - DOM mouse event object
* @param {MouseEvent} event DOM mouse event object
* @returns {void}
*/
handleDocumentMousedown = (event) => {
Expand Down
14 changes: 14 additions & 0 deletions client/components/ContextMenuItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@ import PropTypes from "prop-types";

import * as styles from "./ContextMenuItem.css";

/**
* @returns {boolean} nothing
*/
function noop() {
return false;
}

/**
* @typedef {object} ContextMenuItemProps
* @property {React.ReactNode} children children
* @property {boolean=} disabled - true when disabled, otherwise false
* @property {React.MouseEventHandler<HTMLLIElement>=} onClick on click handler
*/

/**
* @param {ContextMenuItemProps} props props
* @returns {JSX.Element} context menu item
*/
export default function ContextMenuItem({ children, disabled, onClick }) {
const className = cls({
[styles.item]: true,
Expand Down
5 changes: 5 additions & 0 deletions client/components/ModulesTreemap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import Switcher from "./Switcher.jsx";
import Tooltip from "./Tooltip.jsx";
import Treemap from "./Treemap.jsx";

/** @typedef {"statSize" | "parsedSize" | "gzipSize" | "brotliSize" | "zstdSize"} PropSize */

/**
* @returns {{ label: string, prop: PropSize }[]} sizes
*/
function getSizeSwitchItems() {
const items = [
{ label: "Stat", prop: "statSize" },
Expand Down
10 changes: 8 additions & 2 deletions client/components/Treemap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import { Component } from "preact";
import PropTypes from "prop-types";
import { SizeType, ViewerDataType } from "./types.js";

/**
* @param {Event} event event
*/
function preventDefault(event) {
event.preventDefault();
}

/**
* @param {string} str string
* @returns {number} hash
*/
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
Expand Down Expand Up @@ -133,8 +140,7 @@ export default class Treemap extends Component {
},
/**
* Handle Foamtree's "group clicked" event
* @param {FoamtreeEvent} event - Foamtree event object
* (see https://get.carrotsearch.com/foamtree/demo/api/index.html#event-details)
* @param {FoamtreeEvent} event foamtree event object (see https://get.carrotsearch.com/foamtree/demo/api/index.html#event-details)
* @returns {void}
*/
onGroupClick(event) {
Expand Down
5 changes: 5 additions & 0 deletions client/lib/PureComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Component } from "preact";

/**
* @param {object} obj1 obj1
* @param {object} obj2 obj2
* @returns {boolean} true when the same, otherwise false
*/
function isEqual(obj1, obj2) {
if (obj1 === obj2) return true;
const keys = Object.keys(obj1);
Expand Down
15 changes: 15 additions & 0 deletions client/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
/**
* @param {Chunk} chunk chunk
* @returns {boolean} true when chunk is parser, otherwise false
*/
export function isChunkParsed(chunk) {
return typeof chunk.parsedSize === "number";
}

/**
* @param {Module[]} modules modules
* @param {(module: Module) => boolean} cb callback
* @returns {boolean} state
*/
export function walkModules(modules, cb) {
for (const module of modules) {
if (cb(module) === false) return false;
Expand All @@ -12,6 +21,12 @@ export function walkModules(modules, cb) {
}
}

/**
* @template T
* @param {T} elem element
* @param {T[]} container container
* @returns {boolean} true when element is outside, otherwise false
*/
export function elementIsOutside(elem, container) {
return !(elem === container || container.contains(elem));
}
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
Loading