You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
613 lines
17 KiB
613 lines
17 KiB
/* |
|
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 createSchemaValidation = require("./util/create-schema-validation"); |
|
const { contextify } = require("./util/identifier"); |
|
|
|
/** @typedef {import("../declarations/plugins/ProgressPlugin").HandlerFunction} HandlerFunction */ |
|
/** @typedef {import("../declarations/plugins/ProgressPlugin").ProgressPluginArgument} ProgressPluginArgument */ |
|
/** @typedef {import("../declarations/plugins/ProgressPlugin").ProgressPluginOptions} ProgressPluginOptions */ |
|
|
|
const validate = createSchemaValidation( |
|
require("../schemas/plugins/ProgressPlugin.check.js"), |
|
() => require("../schemas/plugins/ProgressPlugin.json"), |
|
{ |
|
name: "Progress Plugin", |
|
baseDataPath: "options" |
|
} |
|
); |
|
const median3 = (a, b, c) => { |
|
return a + b + c - Math.max(a, b, c) - Math.min(a, b, c); |
|
}; |
|
|
|
const createDefaultHandler = (profile, logger) => { |
|
/** @type {{ value: string, time: number }[]} */ |
|
const lastStateInfo = []; |
|
|
|
const defaultHandler = (percentage, msg, ...args) => { |
|
if (profile) { |
|
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 |
|
/* 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); |
|
} |
|
} |
|
} |
|
if (stateItem === undefined) { |
|
lastStateInfo.length = i; |
|
} else { |
|
lastStateItem.value = stateItem; |
|
lastStateItem.time = now; |
|
lastStateInfo.length = i + 1; |
|
} |
|
} |
|
} else { |
|
lastStateInfo[i] = { |
|
value: stateItem, |
|
time: now |
|
}; |
|
} |
|
} |
|
} |
|
logger.status(`${Math.floor(percentage * 100)}%`, msg, ...args); |
|
if (percentage === 1 || (!msg && args.length === 0)) logger.status(); |
|
}; |
|
|
|
return defaultHandler; |
|
}; |
|
|
|
/** |
|
* @callback ReportProgress |
|
* @param {number} p |
|
* @param {...string} [args] |
|
* @returns {void} |
|
*/ |
|
|
|
/** @type {WeakMap<Compiler,ReportProgress>} */ |
|
const progressReporters = new WeakMap(); |
|
|
|
class ProgressPlugin { |
|
/** |
|
* @param {Compiler} compiler the current compiler |
|
* @returns {ReportProgress} a progress reporter, if any |
|
*/ |
|
static getReporter(compiler) { |
|
return progressReporters.get(compiler); |
|
} |
|
|
|
/** |
|
* @param {ProgressPluginArgument} options options |
|
*/ |
|
constructor(options = {}) { |
|
if (typeof options === "function") { |
|
options = { |
|
handler: options |
|
}; |
|
} |
|
|
|
validate(options); |
|
options = { ...ProgressPlugin.defaultOptions, ...options }; |
|
|
|
this.profile = options.profile; |
|
this.handler = options.handler; |
|
this.modulesCount = options.modulesCount; |
|
this.dependenciesCount = options.dependenciesCount; |
|
this.showEntries = options.entries; |
|
this.showModules = options.modules; |
|
this.showDependencies = options.dependencies; |
|
this.showActiveModules = options.activeModules; |
|
this.percentBy = options.percentBy; |
|
} |
|
|
|
/** |
|
* @param {Compiler | MultiCompiler} compiler webpack compiler |
|
* @returns {void} |
|
*/ |
|
apply(compiler) { |
|
const handler = |
|
this.handler || |
|
createDefaultHandler( |
|
this.profile, |
|
compiler.getInfrastructureLogger("webpack.Progress") |
|
); |
|
if (compiler instanceof MultiCompiler) { |
|
this._applyOnMultiCompiler(compiler, handler); |
|
} else if (compiler instanceof Compiler) { |
|
this._applyOnCompiler(compiler, handler); |
|
} |
|
} |
|
|
|
/** |
|
* @param {MultiCompiler} compiler webpack multi-compiler |
|
* @param {HandlerFunction} handler function that executes for every progress step |
|
* @returns {void} |
|
*/ |
|
_applyOnMultiCompiler(compiler, handler) { |
|
const states = compiler.compilers.map( |
|
() => /** @type {[number, ...string[]]} */ ([0]) |
|
); |
|
compiler.compilers.forEach((compiler, idx) => { |
|
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(compiler); |
|
}); |
|
} |
|
|
|
/** |
|
* @param {Compiler} compiler webpack compiler |
|
* @param {HandlerFunction} handler function that executes for every progress step |
|
* @returns {void} |
|
*/ |
|
_applyOnCompiler(compiler, handler) { |
|
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 dependenciesCount = 0; |
|
let entriesCount = 1; |
|
let doneModules = 0; |
|
let doneDependencies = 0; |
|
let doneEntries = 0; |
|
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); |
|
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 { |
|
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(); |
|
}; |
|
|
|
const factorizeAdd = () => { |
|
dependenciesCount++; |
|
if (dependenciesCount < 50 || dependenciesCount % 100 === 0) |
|
updateThrottled(); |
|
}; |
|
|
|
const factorizeDone = () => { |
|
doneDependencies++; |
|
if (doneDependencies < 50 || doneDependencies % 100 === 0) |
|
updateThrottled(); |
|
}; |
|
|
|
const moduleAdd = () => { |
|
modulesCount++; |
|
if (modulesCount < 50 || modulesCount % 100 === 0) updateThrottled(); |
|
}; |
|
|
|
// only used when showActiveModules is set |
|
const moduleBuild = module => { |
|
const ident = module.identifier(); |
|
if (ident) { |
|
activeModules.add(ident); |
|
lastActiveModule = ident; |
|
update(); |
|
} |
|
}; |
|
|
|
const entryAdd = (entry, options) => { |
|
entriesCount++; |
|
if (entriesCount < 5 || entriesCount % 10 === 0) updateThrottled(); |
|
}; |
|
|
|
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(); |
|
}; |
|
|
|
const entryDone = (entry, options) => { |
|
doneEntries++; |
|
update(); |
|
}; |
|
|
|
const cache = compiler |
|
.getCache("ProgressPlugin") |
|
.getItemCache("counts", null); |
|
|
|
let cacheGetPromise; |
|
|
|
compiler.hooks.beforeCompile.tap("ProgressPlugin", () => { |
|
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("ProgressPlugin", compilation => { |
|
if (compilation.compiler.isChild()) return Promise.resolve(); |
|
return cacheGetPromise.then(async oldData => { |
|
if ( |
|
!oldData || |
|
oldData.modulesCount !== modulesCount || |
|
oldData.dependenciesCount !== dependenciesCount |
|
) { |
|
await cache.storePromise({ modulesCount, dependenciesCount }); |
|
} |
|
}); |
|
}); |
|
|
|
compiler.hooks.compilation.tap("ProgressPlugin", compilation => { |
|
if (compilation.compiler.isChild()) return; |
|
lastModulesCount = modulesCount; |
|
lastEntriesCount = entriesCount; |
|
lastDependenciesCount = dependenciesCount; |
|
modulesCount = dependenciesCount = entriesCount = 0; |
|
doneModules = doneDependencies = doneEntries = 0; |
|
|
|
compilation.factorizeQueue.hooks.added.tap( |
|
"ProgressPlugin", |
|
factorizeAdd |
|
); |
|
compilation.factorizeQueue.hooks.result.tap( |
|
"ProgressPlugin", |
|
factorizeDone |
|
); |
|
|
|
compilation.addModuleQueue.hooks.added.tap("ProgressPlugin", moduleAdd); |
|
compilation.processDependenciesQueue.hooks.result.tap( |
|
"ProgressPlugin", |
|
moduleDone |
|
); |
|
|
|
if (showActiveModules) { |
|
compilation.hooks.buildModule.tap("ProgressPlugin", moduleBuild); |
|
} |
|
|
|
compilation.hooks.addEntry.tap("ProgressPlugin", entryAdd); |
|
compilation.hooks.failedEntry.tap("ProgressPlugin", entryDone); |
|
compilation.hooks.succeedEntry.tap("ProgressPlugin", entryDone); |
|
|
|
// avoid dynamic require if bundled with webpack |
|
// @ts-expect-error |
|
if (typeof __webpack_require__ !== "function") { |
|
const requiredLoaders = new Set(); |
|
NormalModule.getCompilationHooks(compilation).beforeLoaders.tap( |
|
"ProgressPlugin", |
|
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; |
|
Object.keys(hooks).forEach((name, idx) => { |
|
const title = hooks[name]; |
|
const percentage = (idx / numberOfHooks) * 0.25 + 0.7; |
|
compilation.hooks[name].intercept({ |
|
name: "ProgressPlugin", |
|
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: "ProgressPlugin", |
|
call() { |
|
handler(0.1, "building"); |
|
}, |
|
done() { |
|
handler(0.65, "building"); |
|
} |
|
}); |
|
const interceptHook = (hook, progress, category, name) => { |
|
hook.intercept({ |
|
name: "ProgressPlugin", |
|
call() { |
|
handler(progress, category, name); |
|
}, |
|
done() { |
|
progressReporters.set(compiler, undefined); |
|
handler(progress, category, name); |
|
}, |
|
result() { |
|
handler(progress, category, name); |
|
}, |
|
error() { |
|
handler(progress, category, name); |
|
}, |
|
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: "ProgressPlugin", |
|
call() { |
|
handler(0, ""); |
|
} |
|
}); |
|
interceptHook(compiler.cache.hooks.endIdle, 0.01, "cache", "end idle"); |
|
compiler.hooks.beforeRun.intercept({ |
|
name: "ProgressPlugin", |
|
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: "ProgressPlugin", |
|
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: "ProgressPlugin", |
|
done() { |
|
handler(1, ""); |
|
} |
|
}); |
|
compiler.cache.hooks.shutdown.intercept({ |
|
name: "ProgressPlugin", |
|
done() { |
|
handler(1, ""); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
ProgressPlugin.defaultOptions = { |
|
profile: false, |
|
modulesCount: 5000, |
|
dependenciesCount: 10000, |
|
modules: true, |
|
dependencies: true, |
|
activeModules: false, |
|
entries: true |
|
}; |
|
|
|
module.exports = ProgressPlugin;
|
|
|