UNPKG

webpack-bundle-analyzer

Version:

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

314 lines (297 loc) 9.52 kB
"use strict"; const fs = require("node:fs"); const http = require("node:http"); const path = require("node:path"); const { bold } = require("picocolors"); const sirv = require("sirv"); const WebSocket = require("ws"); const Logger = require("./Logger"); const analyzer = require("./analyzer"); const { renderViewer } = require("./template"); const { open } = require("./utils"); /** @typedef {import("http").Server} Server */ /** @typedef {import("ws").WebSocketServer} WebSocketServer */ /** @typedef {import("webpack").StatsCompilation} StatsCompilation */ /** @typedef {import("./BundleAnalyzerPlugin").Sizes} Sizes */ /** @typedef {import("./BundleAnalyzerPlugin").CompressionAlgorithm} CompressionAlgorithm */ /** @typedef {import("./BundleAnalyzerPlugin").ReportTitle} ReportTitle */ /** @typedef {import("./BundleAnalyzerPlugin").AnalyzerUrl} AnalyzerUrl */ /** @typedef {import("./BundleAnalyzerPlugin").ExcludeAssets} ExcludeAssets */ /** @typedef {import("./analyzer").ViewerDataOptions} ViewerDataOptions */ /** @typedef {import("./analyzer").ChartData} ChartData */ const projectRoot = path.resolve(__dirname, ".."); /** * @param {string | (() => string)} reportTitle report title * @returns {string} resolved title */ function resolveTitle(reportTitle) { if (typeof reportTitle === "function") { return reportTitle(); } return reportTitle; } /** * @param {Sizes} defaultSizes default sizes * @param {CompressionAlgorithm} compressionAlgorithm compression algorithm * @returns {Sizes} default sizes */ function resolveDefaultSizes(defaultSizes, compressionAlgorithm) { if (["gzip", "brotli", "zstd"].includes(defaultSizes)) { return compressionAlgorithm; } return defaultSizes; } /** @typedef {(string | undefined | null)[]} Entrypoints */ /** * @param {StatsCompilation} bundleStats bundle stats * @returns {Entrypoints} entrypoints */ function getEntrypoints(bundleStats) { if (bundleStats === null || bundleStats === undefined || !bundleStats.entrypoints) { return []; } return Object.values(bundleStats.entrypoints).map(entrypoint => entrypoint.name); } /** * @param {ViewerDataOptions} analyzerOpts analyzer options * @param {StatsCompilation} bundleStats bundle stats * @param {string | null} bundleDir bundle dir * @returns {ChartData | null} chart data */ function getChartData(analyzerOpts, bundleStats, bundleDir) { /** @type {ChartData | undefined | null} */ let chartData; const { logger } = analyzerOpts; try { chartData = analyzer.getViewerData(bundleStats, bundleDir, analyzerOpts); } catch (err) { logger.error(`Couldn't analyze webpack bundle:\n${err}`); logger.debug(/** @type {Error} */err.stack); chartData = null; } // chartData can either be an array (bundleInfo[]) or null. It can't be an plain object anyway if ( // analyzer.getViewerData() doesn't failed in the previous step chartData && !Array.isArray(chartData)) { logger.error("Couldn't find any javascript bundles in provided stats file"); chartData = null; } return chartData; } /** * @typedef {object} ServerOptions * @property {number} port port * @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 {Sizes} defaultSizes default sizes * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm * @property {ExcludeAssets | null} excludeAssets exclude assets * @property {ReportTitle} reportTitle report title * @property {AnalyzerUrl} analyzerUrl analyzer url */ /** @typedef {{ ws: WebSocketServer, http: Server, updateChartData: (bundleStats: StatsCompilation) => void }} ViewerServerObj */ /** * @param {StatsCompilation} bundleStats bundle stats * @param {ServerOptions} opts options * @returns {Promise<ViewerServerObj>} server */ async function startServer(bundleStats, opts) { const { port = 8888, host = "127.0.0.1", openBrowser = true, bundleDir = null, logger = new Logger(), defaultSizes = "parsed", compressionAlgorithm, excludeAssets = null, reportTitle, analyzerUrl } = opts || {}; const analyzerOpts = { logger, excludeAssets, compressionAlgorithm }; let chartData = getChartData(analyzerOpts, bundleStats, bundleDir); if (!chartData) { throw new Error("Can't get chart data"); } const sirvMiddleware = sirv(`${projectRoot}/public`, { // disables caching and traverse the file system on every request dev: true }); const entrypoints = getEntrypoints(bundleStats); const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/") { const html = renderViewer({ mode: "server", title: resolveTitle(reportTitle), chartData: (/** @type {ChartData} */chartData), entrypoints, defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm), compressionAlgorithm, enableWebSocket: true }); res.writeHead(200, { "Content-Type": "text/html" }); res.end(html); } else { sirvMiddleware(req, res); } }); await new Promise( /** * @param {(value: void) => void} resolve resolve */ resolve => { server.listen(port, host, () => { resolve(); const url = analyzerUrl({ listenPort: port, listenHost: host, boundAddress: server.address() }); logger.info(`${bold("Webpack Bundle Analyzer")} is started at ${bold(url)}\n` + `Use ${bold("Ctrl+C")} to close it`); if (openBrowser) { open(url, logger); } }); }); const wss = new WebSocket.Server({ server }); wss.on("connection", ws => { ws.on("error", err => { // Ignore network errors like `ECONNRESET`, `EPIPE`, etc. if (/** @type {NodeJS.ErrnoException} */err.errno) return; logger.info(err.message); }); }); /** * @param {StatsCompilation} bundleStats bundle stats */ function updateChartData(bundleStats) { const newChartData = getChartData(analyzerOpts, bundleStats, bundleDir); if (!newChartData) return; chartData = newChartData; for (const client of wss.clients) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ event: "chartDataUpdated", data: newChartData })); } } } return { ws: wss, http: server, updateChartData }; } /** * @typedef {object} GenerateReportOptions * @property {boolean} openBrowser true when need to open browser, otherwise false * @property {string} reportFilename report filename * @property {ReportTitle} reportTitle report title * @property {string | null} bundleDir bundle dir * @property {Logger} logger logger * @property {Sizes} defaultSizes default sizes * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm * @property {ExcludeAssets} excludeAssets exclude assets */ /** * @param {StatsCompilation} bundleStats bundle stats * @param {GenerateReportOptions} opts opts * @returns {Promise<void>} */ async function generateReport(bundleStats, opts) { const { openBrowser = true, reportFilename, reportTitle, bundleDir = null, logger = new Logger(), defaultSizes = "parsed", compressionAlgorithm, excludeAssets = null } = opts || {}; const chartData = getChartData({ logger, excludeAssets, compressionAlgorithm }, bundleStats, bundleDir); const entrypoints = getEntrypoints(bundleStats); if (!chartData) return; const reportHtml = renderViewer({ mode: "static", title: resolveTitle(reportTitle), chartData, entrypoints, defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm), compressionAlgorithm, enableWebSocket: false }); const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename); fs.mkdirSync(path.dirname(reportFilepath), { recursive: true }); fs.writeFileSync(reportFilepath, reportHtml); logger.info(`${bold("Webpack Bundle Analyzer")} saved report to ${bold(reportFilepath)}`); if (openBrowser) { open(`file://${reportFilepath}`, logger); } } /** * @typedef {object} GenerateJSONReportOptions * @property {string} reportFilename report filename * @property {string | null} bundleDir bundle dir * @property {Logger} logger logger * @property {ExcludeAssets} excludeAssets exclude assets * @property {CompressionAlgorithm} compressionAlgorithm compression algorithm */ /** * @param {StatsCompilation} bundleStats bundle stats * @param {GenerateJSONReportOptions} opts options * @returns {Promise<void>} */ async function generateJSONReport(bundleStats, opts) { const { reportFilename, bundleDir = null, logger = new Logger(), excludeAssets = null, compressionAlgorithm } = opts || {}; const chartData = getChartData({ logger, excludeAssets, compressionAlgorithm }, bundleStats, bundleDir); if (!chartData) return; 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)}`); } module.exports = { generateJSONReport, generateReport, getEntrypoints, // deprecated start: startServer, startServer };