UNPKG

next

Version:

The React Framework

190 lines (189 loc) 8.58 kB
"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