/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v5.108.0
lib/ProgressPlugin.js
820 строк
23 KB
Alexander Akait
refactor: annotate constructor fields with @type to improve type coverage (#21265)
24 июн 2026, 13:36
Не верифицирован
24 июн 2026, 13:36
e8f9334
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const Compiler = require("./Compiler"); const MultiCompiler = require("./MultiCompiler"); const NormalModule = require("./NormalModule"); const { contextify } = require("./util/identifier"); const memoize = require("./util/memoize"); const getColors = memoize(() => { const cli = require("./cli"); return cli.createColors({ useColor: cli.isColorSupported() }); }); const BAR_LENGTH = 25; const BLOCK_CHAR = "━"; const BULLET_ICON = "●"; /** @typedef {import("tapable").Tap} Tap */ /** * Defines the hook type used by this module. * @template T, R, AdditionalOptions * @typedef {import("tapable").Hook<T, R, AdditionalOptions>} Hook */ /** @typedef {import("../declarations/plugins/ProgressPlugin").ProgressPluginArgument} ProgressPluginArgument */ /** @typedef {import("../declarations/plugins/ProgressPlugin").ProgressPluginOptions} ProgressPluginOptions */ /** @typedef {import("./Compilation").FactorizeModuleOptions} FactorizeModuleOptions */ /** @typedef {import("./Dependency")} Dependency */ /** @typedef {import("./Entrypoint").EntryOptions} EntryOptions */ /** @typedef {import("./Module")} Module */ /** @typedef {import("./ModuleFactory").ModuleFactoryResult} ModuleFactoryResult */ /** @typedef {import("./logging/Logger").Logger} Logger */ /** @typedef {import("./cli").Colors} Colors */ /** * Defines the async queue type used by this module. * @template T, K, R * @typedef {import("./util/AsyncQueue")<T, K, R>} AsyncQueue */ /** * Defines the counts data type used by this module. * @typedef {object} CountsData * @property {number} modulesCount modules count * @property {number} dependenciesCount dependencies count */ /** * Returns median. * @param {number} a a * @param {number} b b * @param {number} c c * @returns {number} median */ const median3 = (a, b, c) => a + b + c - Math.max(a, b, c) - Math.min(a, b, c); /** @typedef {(percentage: number, msg: string, ...args: string[]) => void} HandlerFn */ /** * @param {Logger} logger logger * @param {{ value: string | undefined, time: number }[]} lastStateInfo mutable state * @param {number} percentage percentage * @param {string} msg msg * @param {string[]} args args */ const reportProfile = (logger, lastStateInfo, percentage, msg, args) => { if (percentage === 0) { lastStateInfo.length = 0; } const fullState = [msg, ...args]; const state = fullState.map((s) => s.replace(/\d+\/\d+ /g, "")); const now = Date.now(); const len = Math.max(state.length, lastStateInfo.length); for (let i = len; i >= 0; i--) { const stateItem = i < state.length ? state[i] : undefined; const lastStateItem = i < lastStateInfo.length ? lastStateInfo[i] : undefined; if (lastStateItem) { if (stateItem !== lastStateItem.value) { const diff = now - lastStateItem.time; if (lastStateItem.value) { let reportState = lastStateItem.value; if (i > 0) { reportState = `${lastStateInfo[i - 1].value} > ${reportState}`; } const stateMsg = `${" | ".repeat(i)}${diff} ms ${reportState}`; const d = diff; // This depends on timing so we ignore it for coverage /* eslint-disable no-lone-blocks */ /* istanbul ignore next */ { if (d > 10000) { logger.error(stateMsg); } else if (d > 1000) { logger.warn(stateMsg); } else if (d > 10) { logger.info(stateMsg); } else if (d > 5) { logger.log(stateMsg); } else { logger.debug(stateMsg); } } /* eslint-enable no-lone-blocks */ } if (stateItem === undefined) { lastStateInfo.length = i; } else { lastStateItem.value = stateItem; lastStateItem.time = now; lastStateInfo.length = i + 1; } } } else { lastStateInfo[i] = { value: stateItem, time: now }; } } }; /** * @param {string} name progress bar name * @param {string} color progress bar color * @returns {(percentage: number) => string} bar renderer */ const createReportBar = (name, color) => { const c = getColors(); return (percentage) => { const w = Math.round(percentage * BAR_LENGTH); const filled = BLOCK_CHAR.repeat(w); const empty = BLOCK_CHAR.repeat(BAR_LENGTH - w); const colorFn = color in c ? c[/** @type {keyof Colors} */ (color)] : c.green; return `${[BULLET_ICON, name, filled].map(colorFn).join(" ")}${c.white(empty)}`; }; }; /** @typedef {Required<Exclude<NonNullable<ProgressPluginOptions["progressBar"]>, boolean>>} ProgressBarOptions */ /** * Creates a default handler. * @param {boolean | null | undefined} profile need profile * @param {Logger} logger logger * @param {ProgressBarOptions | false=} progressBar render bar * @returns {HandlerFn} default handler */ const createDefaultHandler = (profile, logger, progressBar) => { /** @type {{ value: string | undefined, time: number }[]} */ const lastStateInfo = []; /** @type {HandlerFn} */ const defaultHandler = (percentage, msg, ...args) => { if (profile) { reportProfile(logger, lastStateInfo, percentage, msg, args); } if (progressBar) { const reportBar = createReportBar(progressBar.name, progressBar.color); const c = getColors(); /** @type {string} */ const currentBar = reportBar(percentage); if (percentage === 1) { logger.status(); } else if (msg) { logger.status( `${currentBar} (${Math.floor(percentage * 100)}%)`, `\n${[msg, ...args].map(c.gray).join(" ")}` ); } else { logger.status(`${currentBar} (${Math.floor(percentage * 100)}%)`); } return; } logger.status(`${Math.floor(percentage * 100)}%`, msg, ...args); if (percentage === 1 || (!msg && args.length === 0)) logger.status(); }; return defaultHandler; }; const SKIPPED_QUEUE_CONTEXTS = ["import-module", "load-module"]; /** * Defines the report progress callback. * @callback ReportProgress * @param {number} p percentage * @param {...string} args additional arguments * @returns {void} */ /** @type {WeakMap<Compiler, ReportProgress | undefined>} */ const progressReporters = new WeakMap(); const PLUGIN_NAME = "ProgressPlugin"; /** @type {Required<Omit<ProgressPluginOptions, "handler">>} */ const DEFAULT_OPTIONS = { profile: false, modulesCount: 5000, dependenciesCount: 10000, modules: true, dependencies: true, activeModules: false, entries: true, percentBy: null, progressBar: false }; class ProgressPlugin { /** * Returns a progress reporter, if any. * @param {Compiler} compiler the current compiler * @returns {ReportProgress | undefined} a progress reporter, if any */ static getReporter(compiler) { return progressReporters.get(compiler); } /** * Creates an instance of ProgressPlugin. * @param {ProgressPluginArgument} options options */ constructor(options = {}) { if (typeof options === "function") { options = { handler: options }; } /** @type {ProgressPluginOptions} */ this.options = options; const merged = { ...DEFAULT_OPTIONS, ...options }; /** @type {boolean | null} */ this.profile = merged.profile; /** @type {HandlerFn | undefined} */ this.handler = merged.handler; /** @type {number} */ this.modulesCount = merged.modulesCount; /** @type {number} */ this.dependenciesCount = merged.dependenciesCount; /** @type {boolean} */ this.showEntries = merged.entries; /** @type {boolean} */ this.showModules = merged.modules; /** @type {boolean} */ this.showDependencies = merged.dependencies; /** @type {boolean} */ this.showActiveModules = merged.activeModules; this.percentBy = merged.percentBy; const progressBar = merged.progressBar === true ? {} : merged.progressBar; /** @type {ProgressBarOptions | false} */ this.progressBar = progressBar ? { name: "Build", color: "green", ...progressBar } : false; } /** * Applies the plugin by registering its hooks on the compiler. * @param {Compiler | MultiCompiler} compiler webpack compiler * @returns {void} */ apply(compiler) { const handler = this.handler || createDefaultHandler( this.profile, compiler.getInfrastructureLogger("webpack.Progress"), this.progressBar ); if (compiler instanceof MultiCompiler) { this._applyOnMultiCompiler(compiler, handler); } else if (compiler instanceof Compiler) { this._applyOnCompiler(compiler, handler); } } /** * Apply on multi compiler. * @param {MultiCompiler} compiler webpack multi-compiler * @param {HandlerFn} handler function that executes for every progress step * @returns {void} */ _applyOnMultiCompiler(compiler, handler) { const states = compiler.compilers.map( () => /** @type {[number, ...string[]]} */ ([0]) ); for (const [idx, item] of compiler.compilers.entries()) { new ProgressPlugin((p, msg, ...args) => { states[idx] = [p, msg, ...args]; let sum = 0; for (const [p] of states) sum += p; handler(sum / states.length, `[${idx}] ${msg}`, ...args); }).apply(item); } } /** * Processes the provided compiler. * @param {Compiler} compiler webpack compiler * @param {HandlerFn} handler function that executes for every progress step * @returns {void} */ _applyOnCompiler(compiler, handler) { compiler.hooks.validate.tap(PLUGIN_NAME, () => { compiler.validate( () => require("../schemas/plugins/ProgressPlugin.json"), this.options, { name: "Progress Plugin", baseDataPath: "options" }, (options) => require("../schemas/plugins/ProgressPlugin.check")(options) ); }); const showEntries = this.showEntries; const showModules = this.showModules; const showDependencies = this.showDependencies; const showActiveModules = this.showActiveModules; let lastActiveModule = ""; let currentLoader = ""; let lastModulesCount = 0; let lastDependenciesCount = 0; let lastEntriesCount = 0; let modulesCount = 0; let skippedModulesCount = 0; let dependenciesCount = 0; let skippedDependenciesCount = 0; let entriesCount = 1; let doneModules = 0; let doneDependencies = 0; let doneEntries = 0; /** @type {Set<string>} */ const activeModules = new Set(); let lastUpdate = 0; const updateThrottled = () => { if (lastUpdate + 500 < Date.now()) update(); }; const update = () => { /** @type {string[]} */ const items = []; const percentByModules = doneModules / Math.max(lastModulesCount || this.modulesCount || 1, modulesCount); const percentByEntries = doneEntries / Math.max(lastEntriesCount || this.dependenciesCount || 1, entriesCount); const percentByDependencies = doneDependencies / Math.max(lastDependenciesCount || 1, dependenciesCount); /** @type {number} */ let percentageFactor; switch (this.percentBy) { case "entries": percentageFactor = percentByEntries; break; case "dependencies": percentageFactor = percentByDependencies; break; case "modules": percentageFactor = percentByModules; break; default: percentageFactor = median3( percentByModules, percentByEntries, percentByDependencies ); } const percentage = 0.1 + percentageFactor * 0.55; if (currentLoader) { items.push( `import loader ${contextify( compiler.context, currentLoader, compiler.root )}` ); } else { /** @type {string[]} */ const statItems = []; if (showEntries) { statItems.push(`${doneEntries}/${entriesCount} entries`); } if (showDependencies) { statItems.push( `${doneDependencies}/${dependenciesCount} dependencies` ); } if (showModules) { statItems.push(`${doneModules}/${modulesCount} modules`); } if (showActiveModules) { statItems.push(`${activeModules.size} active`); } if (statItems.length > 0) { items.push(statItems.join(" ")); } if (showActiveModules) { items.push(lastActiveModule); } } handler(percentage, "building", ...items); lastUpdate = Date.now(); }; /** * Processes the provided factorize queue. * @template T * @param {AsyncQueue<FactorizeModuleOptions, string, Module | ModuleFactoryResult>} factorizeQueue async queue * @param {T} _item item */ const factorizeAdd = (factorizeQueue, _item) => { if (SKIPPED_QUEUE_CONTEXTS.includes(factorizeQueue.getContext())) { skippedDependenciesCount++; } dependenciesCount++; if (dependenciesCount < 50 || dependenciesCount % 100 === 0) { updateThrottled(); } }; const factorizeDone = () => { doneDependencies++; if (doneDependencies < 50 || doneDependencies % 100 === 0) { updateThrottled(); } }; /** * Processes the provided add module queue. * @template T * @param {AsyncQueue<Module, string, Module>} addModuleQueue async queue * @param {T} _item item */ const moduleAdd = (addModuleQueue, _item) => { if (SKIPPED_QUEUE_CONTEXTS.includes(addModuleQueue.getContext())) { skippedModulesCount++; } modulesCount++; if (modulesCount < 50 || modulesCount % 100 === 0) updateThrottled(); }; // only used when showActiveModules is set /** * Processes the provided module. * @param {Module} module the module */ const moduleBuild = (module) => { const ident = module.identifier(); if (ident) { activeModules.add(ident); lastActiveModule = ident; update(); } }; /** * Processes the provided entry. * @param {Dependency} entry entry dependency * @param {EntryOptions} options options object */ const entryAdd = (entry, options) => { entriesCount++; if (entriesCount < 5 || entriesCount % 10 === 0) updateThrottled(); }; /** * Processes the provided module. * @param {Module} module the module */ const moduleDone = (module) => { doneModules++; if (showActiveModules) { const ident = module.identifier(); if (ident) { activeModules.delete(ident); if (lastActiveModule === ident) { lastActiveModule = ""; for (const m of activeModules) { lastActiveModule = m; } update(); return; } } } if (doneModules < 50 || doneModules % 100 === 0) updateThrottled(); }; /** * Processes the provided entry. * @param {Dependency} entry entry dependency * @param {EntryOptions} options options object */ const entryDone = (entry, options) => { doneEntries++; update(); }; const cache = compiler.getCache(PLUGIN_NAME).getItemCache("counts", null); /** @type {Promise<CountsData> | undefined} */ let cacheGetPromise; compiler.hooks.beforeCompile.tap(PLUGIN_NAME, () => { if (!cacheGetPromise) { cacheGetPromise = cache.getPromise().then( (data) => { if (data) { lastModulesCount = lastModulesCount || data.modulesCount; lastDependenciesCount = lastDependenciesCount || data.dependenciesCount; } return data; }, (_err) => { // Ignore error } ); } }); compiler.hooks.afterCompile.tapPromise(PLUGIN_NAME, (compilation) => { if (compilation.compiler.isChild()) return Promise.resolve(); return /** @type {Promise<CountsData>} */ (cacheGetPromise).then( async (oldData) => { const realModulesCount = modulesCount - skippedModulesCount; const realDependenciesCount = dependenciesCount - skippedDependenciesCount; if ( !oldData || oldData.modulesCount !== realModulesCount || oldData.dependenciesCount !== realDependenciesCount ) { await cache.storePromise({ modulesCount: realModulesCount, dependenciesCount: realDependenciesCount }); } } ); }); compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { if (compilation.compiler.isChild()) return; lastModulesCount = modulesCount; lastEntriesCount = entriesCount; lastDependenciesCount = dependenciesCount; modulesCount = skippedModulesCount = dependenciesCount = skippedDependenciesCount = entriesCount = 0; doneModules = doneDependencies = doneEntries = 0; compilation.factorizeQueue.hooks.added.tap(PLUGIN_NAME, (item) => factorizeAdd(compilation.factorizeQueue, item) ); compilation.factorizeQueue.hooks.result.tap(PLUGIN_NAME, factorizeDone); compilation.addModuleQueue.hooks.added.tap(PLUGIN_NAME, (item) => moduleAdd(compilation.addModuleQueue, item) ); compilation.processDependenciesQueue.hooks.result.tap( PLUGIN_NAME, moduleDone ); if (showActiveModules) { compilation.hooks.buildModule.tap(PLUGIN_NAME, moduleBuild); } compilation.hooks.addEntry.tap(PLUGIN_NAME, entryAdd); compilation.hooks.failedEntry.tap(PLUGIN_NAME, entryDone); compilation.hooks.succeedEntry.tap(PLUGIN_NAME, entryDone); // @ts-expect-error avoid dynamic require if bundled with webpack if (typeof __webpack_require__ !== "function") { /** @type {Set<string>} */ const requiredLoaders = new Set(); NormalModule.getCompilationHooks(compilation).beforeLoaders.tap( PLUGIN_NAME, (loaders) => { for (const loader of loaders) { if ( loader.type !== "module" && !requiredLoaders.has(loader.loader) ) { requiredLoaders.add(loader.loader); currentLoader = loader.loader; update(); require(loader.loader); } } if (currentLoader) { currentLoader = ""; update(); } } ); } const hooks = { finishModules: "finish module graph", seal: "plugins", optimizeDependencies: "dependencies optimization", afterOptimizeDependencies: "after dependencies optimization", beforeChunks: "chunk graph", afterChunks: "after chunk graph", optimize: "optimizing", optimizeModules: "module optimization", afterOptimizeModules: "after module optimization", optimizeChunks: "chunk optimization", afterOptimizeChunks: "after chunk optimization", optimizeTree: "module and chunk tree optimization", afterOptimizeTree: "after module and chunk tree optimization", optimizeChunkModules: "chunk modules optimization", afterOptimizeChunkModules: "after chunk modules optimization", reviveModules: "module reviving", beforeModuleIds: "before module ids", moduleIds: "module ids", optimizeModuleIds: "module id optimization", afterOptimizeModuleIds: "module id optimization", reviveChunks: "chunk reviving", beforeChunkIds: "before chunk ids", chunkIds: "chunk ids", optimizeChunkIds: "chunk id optimization", afterOptimizeChunkIds: "after chunk id optimization", recordModules: "record modules", recordChunks: "record chunks", beforeModuleHash: "module hashing", beforeCodeGeneration: "code generation", beforeRuntimeRequirements: "runtime requirements", beforeHash: "hashing", afterHash: "after hashing", recordHash: "record hash", beforeModuleAssets: "module assets processing", beforeChunkAssets: "chunk assets processing", processAssets: "asset processing", afterProcessAssets: "after asset optimization", record: "recording", afterSeal: "after seal" }; const numberOfHooks = Object.keys(hooks).length; for (const [idx, name] of Object.keys(hooks).entries()) { const title = hooks[/** @type {keyof typeof hooks} */ (name)]; const percentage = (idx / numberOfHooks) * 0.25 + 0.7; compilation.hooks[/** @type {keyof typeof hooks} */ (name)].intercept({ name: PLUGIN_NAME, call() { handler(percentage, "sealing", title); }, done() { progressReporters.set(compiler, undefined); handler(percentage, "sealing", title); }, result() { handler(percentage, "sealing", title); }, error() { handler(percentage, "sealing", title); }, tap(tap) { // p is percentage from 0 to 1 // args is any number of messages in a hierarchical matter progressReporters.set(compilation.compiler, (p, ...args) => { handler(percentage, "sealing", title, tap.name, ...args); }); handler(percentage, "sealing", title, tap.name); } }); } }); compiler.hooks.make.intercept({ name: PLUGIN_NAME, call() { handler(0.1, "building"); }, done() { handler(0.65, "building"); } }); /** * Processes the provided hook. * @template {Hook<EXPECTED_ANY, EXPECTED_ANY, EXPECTED_ANY>} T * @param {T} hook hook * @param {number} progress progress from 0 to 1 * @param {string} category category * @param {string} name name */ const interceptHook = (hook, progress, category, name) => { hook.intercept({ name: PLUGIN_NAME, call() { handler(progress, category, name); }, done() { progressReporters.set(compiler, undefined); handler(progress, category, name); }, result() { handler(progress, category, name); }, error() { handler(progress, category, name); }, /** * Processes the provided tap. * @param {Tap} tap tap */ tap(tap) { progressReporters.set(compiler, (p, ...args) => { handler(progress, category, name, tap.name, ...args); }); handler(progress, category, name, tap.name); } }); }; compiler.cache.hooks.endIdle.intercept({ name: PLUGIN_NAME, call() { handler(0, ""); } }); interceptHook(compiler.cache.hooks.endIdle, 0.01, "cache", "end idle"); compiler.hooks.beforeRun.intercept({ name: PLUGIN_NAME, call() { handler(0, ""); } }); interceptHook(compiler.hooks.beforeRun, 0.01, "setup", "before run"); interceptHook(compiler.hooks.run, 0.02, "setup", "run"); interceptHook(compiler.hooks.watchRun, 0.03, "setup", "watch run"); interceptHook( compiler.hooks.normalModuleFactory, 0.04, "setup", "normal module factory" ); interceptHook( compiler.hooks.contextModuleFactory, 0.05, "setup", "context module factory" ); interceptHook( compiler.hooks.beforeCompile, 0.06, "setup", "before compile" ); interceptHook(compiler.hooks.compile, 0.07, "setup", "compile"); interceptHook(compiler.hooks.thisCompilation, 0.08, "setup", "compilation"); interceptHook(compiler.hooks.compilation, 0.09, "setup", "compilation"); interceptHook(compiler.hooks.finishMake, 0.69, "building", "finish"); interceptHook(compiler.hooks.emit, 0.95, "emitting", "emit"); interceptHook(compiler.hooks.afterEmit, 0.98, "emitting", "after emit"); interceptHook(compiler.hooks.done, 0.99, "done", "plugins"); compiler.hooks.done.intercept({ name: PLUGIN_NAME, done() { handler(0.99, ""); } }); interceptHook( compiler.cache.hooks.storeBuildDependencies, 0.99, "cache", "store build dependencies" ); interceptHook(compiler.cache.hooks.shutdown, 0.99, "cache", "shutdown"); interceptHook(compiler.cache.hooks.beginIdle, 0.99, "cache", "begin idle"); interceptHook( compiler.hooks.watchClose, 0.99, "end", "closing watch compilation" ); compiler.cache.hooks.beginIdle.intercept({ name: PLUGIN_NAME, done() { handler(1, ""); } }); compiler.cache.hooks.shutdown.intercept({ name: PLUGIN_NAME, done() { handler(1, ""); } }); } } ProgressPlugin.defaultOptions = DEFAULT_OPTIONS; ProgressPlugin.createDefaultHandler = createDefaultHandler; module.exports = ProgressPlugin;