UNPKG

rollup-plugin-visualizer

Version:

Visualize and analyze your bundle to quickly see which modules are taking up space.

177 lines (176 loc) 8.3 kB
import { promises as fs } from "node:fs"; import path from "node:path"; import opn from "open"; import { version } from "./version.js"; import { createGzipSizeGetter, createBrotliSizeGetter } from "./compress.js"; import { ModuleMapper, replaceHashPlaceholders } from "./module-mapper.js"; import { addLinks, buildTree, mergeTrees } from "./data.js"; import { getSourcemapModules } from "./sourcemap.js"; import { renderTemplate } from "./render-template.js"; import { createFilter } from "../shared/create-filter.js"; const WARN_SOURCEMAP_DISABLED = "rollup output configuration missing sourcemap = true. You should add output.sourcemap = true or disable sourcemap in this plugin"; const WARN_SOURCEMAP_MISSING = (id) => `${id} missing source map`; const WARN_JSON_DEPRECATED = 'Option `json` deprecated, please use template: "raw-data"'; const ERR_FILENAME_EMIT = "When using emitFile option, filename must not be path but a filename"; const defaultSizeGetter = () => Promise.resolve(0); const chooseDefaultFileName = (opts) => { if (opts.filename) return opts.filename; if (opts.json || opts.template === "raw-data") return "stats.json"; if (opts.template === "list") return "stats.yml"; if (opts.template === "markdown") return "stats.md"; return "stats.html"; }; export const visualizer = (opts = {}) => { return { name: "visualizer", async generateBundle(outputOptions, outputBundle) { opts = typeof opts === "function" ? opts(outputOptions) : opts; if ("json" in opts) { this.warn(WARN_JSON_DEPRECATED); if (opts.json) opts.template = "raw-data"; } const filename = opts.filename ?? chooseDefaultFileName(opts); const title = opts.title ?? "Rollup Visualizer"; const open = !!opts.open; const openOptions = opts.openOptions ?? {}; const template = opts.template ?? "treemap"; const projectRoot = opts.projectRoot ?? process.cwd(); const filter = createFilter(opts.include, opts.exclude); const gzipSizeRequested = !!opts.gzipSize; const brotliSizeRequested = !!opts.brotliSize; const gzipSize = gzipSizeRequested && !opts.sourcemap; const brotliSize = brotliSizeRequested && !opts.sourcemap; const gzipSizeGetter = gzipSize ? createGzipSizeGetter(typeof opts.gzipSize === "object" ? opts.gzipSize : {}) : defaultSizeGetter; const brotliSizeGetter = brotliSize ? createBrotliSizeGetter(typeof opts.brotliSize === "object" ? opts.brotliSize : {}) : defaultSizeGetter; const getModuleLengths = async ({ id, renderedLength, code, }, useRenderedLength = false) => { const isCodeEmpty = code == null || code == ""; const result = { id, gzipLength: isCodeEmpty ? 0 : await gzipSizeGetter(code), brotliLength: isCodeEmpty ? 0 : await brotliSizeGetter(code), renderedLength: useRenderedLength ? renderedLength : isCodeEmpty ? 0 : Buffer.byteLength(code, "utf-8"), }; return result; }; if (opts.sourcemap && !outputOptions.sourcemap) { this.warn(WARN_SOURCEMAP_DISABLED); } const roots = []; const mapper = new ModuleMapper(projectRoot); // collect trees for (const [bundleId, bundle] of Object.entries(outputBundle)) { if (bundle.type !== "chunk") continue; //only chunks let tree; if (opts.sourcemap) { if (!bundle.map) { this.warn(WARN_SOURCEMAP_MISSING(bundleId)); } const modules = await getSourcemapModules(bundleId, bundle, outputOptions.dir ?? (outputOptions.file && path.dirname(outputOptions.file)) ?? process.cwd()); const moduleRenderInfo = await Promise.all(Object.values(modules) .filter(({ id }) => filter(bundleId, id)) .map(({ id, renderedLength, code }) => { return getModuleLengths({ id, renderedLength, code: code.join("") }, true); })); tree = buildTree(bundleId, moduleRenderInfo, mapper); } else { const modules = await Promise.all(Object.entries(bundle.modules) .filter(([id]) => filter(bundleId, id)) .map(([id, { renderedLength, code }]) => getModuleLengths({ id, renderedLength, code }), false)); tree = buildTree(bundleId, modules, mapper); } if (tree.children.length === 0) { const bundleSizes = await getModuleLengths({ id: bundleId, renderedLength: bundle.code.length, code: bundle.code, }, false); const facadeModuleId = bundle.facadeModuleId ?? `${bundleId}-unknown`; const bundleUid = mapper.setNodePart(bundleId, facadeModuleId, bundleSizes); mapper.setNodeMeta(facadeModuleId, { isEntry: true }); const leaf = { name: bundleId, uid: bundleUid }; roots.push(leaf); } else { roots.push(tree); } } // after trees we process links (this is mostly for uids) for (const [, bundle] of Object.entries(outputBundle)) { if (bundle.type !== "chunk" || bundle.facadeModuleId == null) continue; //only chunks addLinks(bundle.facadeModuleId, this.getModuleInfo.bind(this), mapper); } const tree = mergeTrees(roots); const data = { version, tree, nodeParts: mapper.getNodeParts(), nodeMetas: mapper.getNodeMetas(), env: { rollup: this.meta.rollupVersion, }, options: { gzip: gzipSize, brotli: brotliSize, sourcemap: !!opts.sourcemap, }, }; const stringData = replaceHashPlaceholders(data); const fileContent = await renderTemplate(template, { title, data: stringData, reportConfig: { sourcemap: !!opts.sourcemap, outputSourcemap: !!outputOptions.sourcemap, gzipSize: { requested: gzipSizeRequested, enabled: gzipSize, }, brotliSize: { requested: brotliSizeRequested, enabled: brotliSize, }, include: opts.include, exclude: opts.exclude, }, }); if (opts.emitFile) { // Regex checks for filenames starting with `./`, `../`, `.\` or `..\` // to account for windows-style path separators if (path.isAbsolute(filename) || /^\.{1,2}[/\\]/.test(filename)) { this.error(ERR_FILENAME_EMIT); } this.emitFile({ type: "asset", fileName: filename, source: fileContent, }); } else { await fs.mkdir(path.dirname(filename), { recursive: true }); await fs.writeFile(filename, fileContent); if (open) { await opn(filename, openOptions); } } }, }; }; export default visualizer;