webpack-split-chunks-analyzer
Version:
Debug how your application is dynamically loaded by webpack
155 lines • 8.06 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SplitChunksAnalyzerPlugin = void 0;
const tslib_1 = require("tslib");
const node_fs_1 = require("node:fs");
const node_path_1 = tslib_1.__importDefault(require("node:path"));
const node_child_process_1 = require("node:child_process");
const node_util_1 = require("node:util");
const transducist_1 = require("transducist");
const dagre_1 = tslib_1.__importDefault(require("@dagrejs/dagre"));
const pretty_bytes_1 = tslib_1.__importDefault(require("pretty-bytes"));
const cheerio = tslib_1.__importStar(require("cheerio"));
const { writeFile, readFile } = node_fs_1.promises;
;
const DEFAULT_OPTIONS = {
excludePattern: [],
outputFile: "split-chunks-report.html",
openOnFinish: false,
};
class SplitChunksAnalyzerPlugin {
constructor(userOptions = {}) {
this.compiler = undefined;
this.analyze = async (stats) => {
if (!this.compiler) {
throw new Error("did not call apply before trying to analyze");
}
// the included webpack types are a bit lacking so we cast
const compilation = stats.compilation;
const dagreGraph = new dagre_1.default.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ rankdir: "TB" });
const graph = {
buildName: stats.compilation.name ?? stats.hash ?? "<unknown>",
nodes: {},
edges: [],
};
let edgeIdCounter = 0;
const prodAssetsIds = (0, transducist_1.chainFrom)(Object.keys(compilation.assets))
.filter((id) => !id.endsWith(".LICENSE"))
.filter((id) => compilation.assetsInfo.get(id)?.development !== true)
.toSet();
const chunkGroups = compilation.chunkGroups;
const entrypointIds = Array.from(compilation.entrypoints.keys());
(0, transducist_1.chainFrom)(chunkGroups)
.filter((chunkGroup) => chunkGroup.chunks.length > 0 || chunkGroup.getChildren().length > 0)
.forEach((chunkGroup) => {
const splitOriginFileName = chunkGroup.origins[0]?.request?.split("/");
const originName = splitOriginFileName?.[splitOriginFileName.length - 1];
const chunkGroupSize = (0, transducist_1.chainFrom)(chunkGroup.getFiles())
.filter((id) => prodAssetsIds.has(id))
.map((id) => compilation.assets[id])
.map((asset) => asset.size())
.sum();
const chunks = (0, transducist_1.chainFrom)(chunkGroup.chunks)
.map((chunk) => {
const file = Array.from(chunk.files)[0];
const chunkSize = compilation.assets[file].size();
const modules = (0, transducist_1.chainFrom)(chunk.getModules())
.map((mod) => {
const moduleName = typeof mod.userRequest === "string"
? node_path_1.default.relative(compilation.compiler.context, mod.userRequest)
: // TODO make this a real name
"<unknown>";
const moduleSize = mod.size();
return {
name: moduleName,
size: moduleSize,
displaySize: (0, pretty_bytes_1.default)(moduleSize),
};
})
.toArray()
.sort((a, b) => b.size - a.size);
return {
name: file,
size: chunkSize,
displaySize: (0, pretty_bytes_1.default)(chunkSize),
modules,
};
})
.filter((chunk) => prodAssetsIds.has(chunk.name))
.filter((chunk) => !this.options.excludePattern.some(r => r.test(chunk.name)))
.toArray()
.sort((a, b) => b.size - a.size);
const name = chunkGroup.name ?? originName ?? chunkGroup.id;
const displaySize = (0, pretty_bytes_1.default)(chunkGroupSize);
dagreGraph.setNode(chunkGroup.id, { width: 170, height: 55 });
graph.nodes[chunkGroup.id] = {
id: chunkGroup.id,
position: {
x: 0,
y: 0,
},
data: {
label: `${name} (${displaySize})`,
name,
size: chunkGroupSize,
displaySize,
entryPoint: entrypointIds.includes(chunkGroup.id),
chunks,
},
};
const childOrders = (0, transducist_1.chainFrom)(Object.entries(chunkGroup.getChildrenByOrders(stats.compilation.moduleGraph, stats.compilation.chunkGraph)))
.map(([orderType, groups]) => [orderType, new Set(groups.map((group) => group.id))])
.toObject(([orderType, _]) => orderType, ([_, groups]) => groups);
chunkGroup.getChildren().forEach((child) => {
if (child.chunks.length > 0 || child.getChildren().length > 0) {
const isPrefetched = childOrders.prefetch?.has(child.id) ?? false;
const isPreloaded = childOrders.preload?.has(child.id) ?? false;
dagreGraph.setEdge(chunkGroup.id, child.id);
graph.edges.push({
id: (edgeIdCounter++).toString(),
source: chunkGroup.id,
target: child.id,
data: {
kind: isPrefetched ? "prefetch" : isPreloaded ? "preload" : undefined,
},
});
}
});
});
dagre_1.default.layout(dagreGraph);
for (const node of Object.values(graph.nodes)) {
const position = dagreGraph.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
node.position = {
x,
y
};
}
const baseHtml = await readFile(node_path_1.default.resolve(__dirname, "../../dist/index.html"));
let lastPath = undefined;
const outputFiles = Array.isArray(this.options.outputFile) ? this.options.outputFile : [this.options.outputFile];
for (const outputFile of outputFiles) {
const outputFilePath = node_path_1.default.resolve(this.compiler.outputPath, outputFile);
const $ = cheerio.load(baseHtml);
$("head").append(`<script type="application/json" id="data">${JSON.stringify(graph)}</script>`);
await writeFile(outputFilePath, $.html());
lastPath = outputFilePath;
}
if (this.options.openOnFinish && lastPath != null) {
await (0, node_util_1.promisify)(node_child_process_1.exec)(`open ${lastPath}`);
}
};
this.options = { ...DEFAULT_OPTIONS, ...userOptions };
}
apply(compiler) {
this.compiler = compiler;
compiler.hooks.done.tapPromise("SplitChunksAnalyzerPlugin", this.analyze);
}
}
exports.SplitChunksAnalyzerPlugin = SplitChunksAnalyzerPlugin;
//# sourceMappingURL=SplitChunksAnalyzerPlugin.js.map