190 lines
8.4 KiB
JavaScript
190 lines
8.4 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
Object.defineProperty(exports, "default", {
|
|
enumerable: true,
|
|
get: function() {
|
|
return SlowModuleDetectionPlugin;
|
|
}
|
|
});
|
|
const _picocolors = require("../../../lib/picocolors");
|
|
const PLUGIN_NAME = 'SlowModuleDetectionPlugin';
|
|
const TreeSymbols = {
|
|
VERTICAL_LINE: '│ ',
|
|
BRANCH: '├─ '
|
|
};
|
|
const PATH_TRUNCATION_LENGTH = 120;
|
|
// Matches node_modules paths, including pnpm-style paths
|
|
const NODE_MODULES_PATH_PATTERN = /node_modules(?:\/\.pnpm)?\/(.*)/;
|
|
const getModuleIdentifier = (module)=>{
|
|
const debugId = module.debugId;
|
|
return String(debugId);
|
|
};
|
|
const getModuleDisplayName = (module)=>{
|
|
const resourcePath = 'resource' in module && typeof module.resource === 'string' ? module.resource : undefined;
|
|
if (!resourcePath) {
|
|
return undefined;
|
|
}
|
|
let displayPath = resourcePath.replace(process.cwd(), '.');
|
|
const nodeModulesMatch = displayPath.match(NODE_MODULES_PATH_PATTERN);
|
|
if (nodeModulesMatch) {
|
|
return nodeModulesMatch[1];
|
|
}
|
|
return displayPath;
|
|
};
|
|
/**
|
|
* Truncates a path to a maximum length. If the path exceeds this length,
|
|
* it will be truncated in the middle and replaced with '...'.
|
|
*/ function truncatePath(path, maxLength) {
|
|
// If the path length is within the limit, return it as is
|
|
if (path.length <= maxLength) return path;
|
|
// Calculate the available length for the start and end segments after accounting for '...'
|
|
const availableLength = maxLength - 3;
|
|
const startSegmentLength = Math.ceil(availableLength / 2);
|
|
const endSegmentLength = Math.floor(availableLength / 2);
|
|
// Extract the start and end segments of the path
|
|
const startSegment = path.slice(0, startSegmentLength);
|
|
const endSegment = path.slice(-endSegmentLength);
|
|
// Return the truncated path with '...' in the middle
|
|
return `${startSegment}...${endSegment}`;
|
|
}
|
|
class ModuleBuildTimeAnalyzer {
|
|
constructor(options){
|
|
this.options = options;
|
|
this.pendingModules = [];
|
|
this.modules = new Map();
|
|
this.moduleParents = new Map();
|
|
this.moduleChildren = new Map();
|
|
this.isFinalized = false;
|
|
this.moduleBuildTimes = new WeakMap();
|
|
this.buildTimeThresholdMs = options.buildTimeThresholdMs;
|
|
}
|
|
recordModuleBuildTime(module, duration) {
|
|
// Webpack guarantees that no more modules will be built after finishModules hook is called,
|
|
// where we generate the report. This check is just a defensive measure.
|
|
if (this.isFinalized) {
|
|
throw Object.defineProperty(new Error(`Invariant (SlowModuleDetectionPlugin): Module is recorded after the report is generated. This is a Next.js internal bug.`), "__NEXT_ERROR_CODE", {
|
|
value: "E630",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
if (duration < this.buildTimeThresholdMs) {
|
|
return; // Skip fast modules
|
|
}
|
|
this.moduleBuildTimes.set(module, duration);
|
|
this.pendingModules.push(module);
|
|
}
|
|
/**
|
|
* For each slow module, traverses up the dependency chain to find all ancestor modules.
|
|
* Builds a directed graph where:
|
|
* 1. Each slow module and its ancestors become nodes
|
|
* 2. Edges represent "imported by" relationships
|
|
* 3. Root nodes are entry points with no parents
|
|
*
|
|
* The resulting graph allows us to visualize the import chains that led to slow builds.
|
|
*/ prepareReport(compilation) {
|
|
for (const module of this.pendingModules){
|
|
const chain = new Set();
|
|
// Walk up the module graph until we hit a root module (no issuer) to populate the chain
|
|
{
|
|
let currentModule = module;
|
|
chain.add(currentModule);
|
|
while(true){
|
|
const issuerModule = compilation.moduleGraph.getIssuer(currentModule);
|
|
if (!issuerModule) break;
|
|
if (chain.has(issuerModule)) {
|
|
throw Object.defineProperty(new Error(`Invariant (SlowModuleDetectionPlugin): Circular dependency detected in module graph. This is a Next.js internal bug.`), "__NEXT_ERROR_CODE", {
|
|
value: "E631",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
chain.add(issuerModule);
|
|
currentModule = issuerModule;
|
|
}
|
|
}
|
|
// Add all visited modules to our graph and create parent-child relationships
|
|
let previousModule = null;
|
|
for (const currentModule of chain){
|
|
const moduleId = getModuleIdentifier(currentModule);
|
|
if (!this.modules.has(moduleId)) {
|
|
this.modules.set(moduleId, currentModule);
|
|
}
|
|
if (previousModule) {
|
|
this.moduleParents.set(previousModule, currentModule);
|
|
let parentChildren = this.moduleChildren.get(currentModule);
|
|
if (!parentChildren) {
|
|
parentChildren = new Map();
|
|
this.moduleChildren.set(currentModule, parentChildren);
|
|
}
|
|
parentChildren.set(getModuleIdentifier(previousModule), previousModule);
|
|
}
|
|
previousModule = currentModule;
|
|
}
|
|
}
|
|
this.isFinalized = true;
|
|
}
|
|
generateReport(compilation) {
|
|
if (!this.isFinalized) {
|
|
this.prepareReport(compilation);
|
|
}
|
|
// Find root modules (those with no parents)
|
|
const rootModules = [
|
|
...this.modules.values()
|
|
].filter((node)=>!this.moduleParents.has(node));
|
|
const formatModuleNode = (node, depth)=>{
|
|
const moduleName = getModuleDisplayName(node) || '';
|
|
if (!moduleName) {
|
|
return formatChildModules(node, depth);
|
|
}
|
|
const prefix = ' ' + TreeSymbols.VERTICAL_LINE.repeat(depth) + TreeSymbols.BRANCH;
|
|
const moduleText = (0, _picocolors.blue)(truncatePath(moduleName, PATH_TRUNCATION_LENGTH - prefix.length));
|
|
const buildTimeMs = this.moduleBuildTimes.get(node);
|
|
const duration = buildTimeMs ? (0, _picocolors.yellow)(` (${Math.ceil(buildTimeMs)}ms)`) : '';
|
|
return prefix + moduleText + duration + '\n' + formatChildModules(node, depth + 1);
|
|
};
|
|
const formatChildModules = (node, depth)=>{
|
|
const children = this.moduleChildren.get(node);
|
|
if (!children) return '';
|
|
return [
|
|
...children
|
|
].map(([_, child])=>formatModuleNode(child, depth)).join('');
|
|
};
|
|
const report = rootModules.map((root)=>formatModuleNode(root, 0)).join('');
|
|
if (report) {
|
|
console.log((0, _picocolors.green)(`🐌 Detected slow modules while compiling ${this.options.compilerType}:`) + '\n' + report);
|
|
}
|
|
}
|
|
}
|
|
class SlowModuleDetectionPlugin {
|
|
constructor(options){
|
|
this.options = options;
|
|
this.apply = (compiler)=>{
|
|
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation)=>{
|
|
const analyzer = new ModuleBuildTimeAnalyzer(this.options);
|
|
const moduleBuildStartTimes = new WeakMap();
|
|
compilation.hooks.buildModule.tap(PLUGIN_NAME, (module)=>{
|
|
moduleBuildStartTimes.set(module, performance.now());
|
|
});
|
|
compilation.hooks.succeedModule.tap(PLUGIN_NAME, (module)=>{
|
|
const startTime = moduleBuildStartTimes.get(module);
|
|
if (!startTime) {
|
|
throw Object.defineProperty(new Error(`Invariant (SlowModuleDetectionPlugin): Unable to find the start time for a module build. This is a Next.js internal bug.`), "__NEXT_ERROR_CODE", {
|
|
value: "E629",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
analyzer.recordModuleBuildTime(module, performance.now() - startTime);
|
|
});
|
|
compilation.hooks.finishModules.tap(PLUGIN_NAME, ()=>{
|
|
analyzer.generateReport(compilation);
|
|
});
|
|
});
|
|
};
|
|
}
|
|
}
|
|
|
|
//# sourceMappingURL=slow-module-detection-plugin.js.map
|