plugins_analyze_hooks.js

/**
 * @file plugins/analyze/hooks.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 *//** */

const WpwAnalyzePlugin = require("./base");
const { withColor } = require("@spmhome/log-utils");
const { hookRunsWithGlobalError } = require("../../utils/constants");
const { isFunction, epochToMSMS } = require("@spmhome/type-utils");


/**
 * @augments WpwAnalyzePlugin
 */
class WpwHooksAnalyzePlugin extends WpwAnalyzePlugin
{
	/**
	 * @private
	 * @type {string[]}
	 */
	added;
	/**
	 * @private
	 * @type {number}
	 */
	last;
	/**
	 * @private
	 * @type {number}
	 */
	start;


    /**
     * @param {WpwPluginOptions} options Plugin options to be applied
     */
	constructor(options)
	{
		super(options);
		this.added = [];
		this.start = 0;
        this.buildOptions = /** @type {WpwHooksAnalyzePluginOptions} */(this.buildOptions);
	}


	/**
     * @override
     */
	static create = WpwHooksAnalyzePlugin.wrap.bind(this);


    /**
     * @override
     */
    onApply() { this.hookSteps(); }


	/**
	 * @private
	 * @param {WebpackCompilationHookName} hook
	 * @param {Lowercase<WebpackCompilationHookStage>} [processAssetStage]
	 * @param {function(any): any} [cb]
	 */
	addCompilationHook(hook, processAssetStage, cb)
	{
		const compilationHook = this.build.compilation.hooks[hook];
		if (hook === "processAssets" && processAssetStage)
		{
			const stageEnum = `PROCESS_ASSETS_STAGE_${processAssetStage.toUpperCase()}`,
					stage = this.build.wp.Compilation[stageEnum],
					hookName = "compilation_processAssets_" + stageEnum;
			if (!this.added.includes(hookName))
			{
				/** @type {WebpackHook<any, any>} */(
				compilationHook).tap(
					{ stage, name: `${this.name}_${hook}_${processAssetStage}` },
					(/** @type {any} */_arg) => this.writeBuildTag(`${hook}::${processAssetStage}`, true)
				);
				this.added.push(hookName);
			}
		}
		else if (isFunction(/** @type {WebpackHook<any, any>} */(compilationHook).tap))
		{
			const hookName = "compilation_" + hook;
			if (!this.added.includes(hookName))
			{
				/** @type {WebpackHook<any, any>} */(compilationHook).tap(
					`${this.name}_${hook}`, (/** @type {any} */_) =>
				{
					this.writeBuildTag(hook.toString(), true);
					return cb?.(_);
				});
				this.added.push(hookName);
			}
		}
	};


	/**
	 * @private
	 * @param {WebpackCompilerHookName} hook
	 * @param {function(any): any} [cb]
	 */
	addCompilerHook(hook, cb)
	{
		const hookName = "compiler_" + hook;
		if (!this.added.includes(hookName))
		{
			this.compiler.hooks[hook].tap(`${this.name}_${hook}`, () =>
			{
				this.writeBuildTag(hook.toString(), false);
				return cb?.();
			});
			this.added.push(hookName);
		}
	};


	// /**
	//  * @private
	//  * @param {WebpackCompilerAsyncHookName} hook
	//  */
	// addCompilerHookPromise(hook)
	// {
	// 	this.compiler.hooks[hook].tapPromise(`${hook}LogHookPromisePlugin`, async () => this.writeBuildTag(hook, false));
	// };


	/**
	 * @private
	 */
	hookSteps()
	{
		this.addCompilerHook("environment", () => { if (!this.start) { this.start = Date.now(); }});
		this.addCompilerHook("beforeRun", () => { if (!this.start) { this.start = Date.now(); }});
		this.addCompilerHook("done",
			() => { if (!this.build.elapsed) { this.build.elapsed = Date.now() - this.start; }}
		);
		this.addCompilerHook("failed",
			() => { if (!this.build.elapsed) { this.build.elapsed = Date.now() - this.start; }}
		);
		this.addCompilerHook("shutdown",
			() => { if (!this.build.elapsed) { this.build.elapsed = Date.now() - this.start; }}
		);
		if (this.buildOptions.compiler !== false)
		{
			this.addCompilerHook("infrastructureLog");
			this.addCompilerHook("afterEnvironment");
			this.addCompilerHook("entryOption");
			this.addCompilerHook("afterPlugins");
			this.addCompilerHook("afterResolvers");
			this.addCompilerHook("initialize");
			this.addCompilerHook("run");
			this.addCompilerHook("normalModuleFactory");
			this.addCompilerHook("contextModuleFactory");
			this.addCompilerHook("beforeCompile");
			this.addCompilerHook("compile");
		}
		if (this.buildOptions.compilation)
		{
			const _getCompilationHooks = () => (/** @type {WebpackCompilation} */compilation) =>
			{
				// this.compilation = compilation;
				const options = this.buildOptions.compilation;
				if (options.all || options.default || options.processAssets)
				{
					this.addCompilationHook("additionalAssets");
					this.addCompilationHook("processAssets", "additional");
					this.addCompilationHook("processAssets", "pre_process");
					this.addCompilationHook("processAssets", "derived");
					this.addCompilationHook("processAssets", "additions");
					this.addCompilationHook("processAssets", "optimize");
					this.addCompilationHook("processAssets", "optimize_count");
					this.addCompilationHook("processAssets", "optimize_compatibility");
					this.addCompilationHook("processAssets", "optimize_size");
					this.addCompilationHook("processAssets", "dev_tooling");
					this.addCompilationHook("processAssets", "optimize_inline");
					this.addCompilationHook("processAssets", "summarize");
					this.addCompilationHook("processAssets", "optimize_hash");
					this.addCompilationHook("processAssets", "optimize_transfer");
					this.addCompilationHook("processAssets", "analyse");
					this.addCompilationHook("processAssets", "report");
					this.addCompilationHook("afterProcessAssets");
				}
				if (options.all || options.default)
				{
					this.addCompilationHook("beforeCodeGeneration");
					this.addCompilationHook("afterCodeGeneration");
					this.addCompilationHook("beforeRuntimeRequirements");
					this.addCompilationHook("afterRuntimeRequirements");
					this.addCompilationHook("record");
					this.addCompilationHook("seal");
					this.addCompilationHook("afterSeal");
					this.addCompilationHook("needAdditionalSeal");
					this.addCompilationHook("renderManifest");
					this.addCompilationHook("beforeModuleAssets");
					this.addCompilationHook("moduleAsset");
					this.addCompilationHook("assetPath");
					this.addCompilationHook("chunkAsset");
					this.addCompilationHook("beforeChunkAssets");
					this.addCompilationHook("shouldGenerateChunkAssets");
					this.addCompilationHook("needAdditionalPass");
					this.addCompilationHook("childCompiler");
					this.addCompilationHook("log");
					this.addCompilationHook("processWarnings");
					this.addCompilationHook("processErrors");
					this.addCompilationHook("statsNormalize");
					this.addCompilationHook("statsFactory");
					this.addCompilationHook("statsPrinter");
				}
				if (options.all || options.hash)
				{
					this.addCompilationHook("beforeHash");
					this.addCompilationHook("afterHash");
					this.addCompilationHook("beforeModuleHash");
					this.addCompilationHook("afterModuleHash");
					this.addCompilationHook("contentHash");
					this.addCompilationHook("chunkHash");
					this.addCompilationHook("fullHash");
					this.addCompilationHook("recordHash");
				}
				if (options.all || options.optimization)
				{
					this.addCompilationHook("optimizeAssets");
					this.addCompilationHook("afterOptimizeAssets");
				}
				// if (options.deprecated)
				// {
				// 	this.addCompilationHook("statsPreset");
				// 	this.addCompilationHook("additionalChunkAssets");
				// 	this.addCompilationHook("optimizeChunkAssets");
				// 	this.addCompilationHook("afterOptimizeChunkAssets");
				// 	this.addCompilationHook("processAdditionalAssets"); // not deprecated but ~ as processAssets|ADDITIONAL
				// }
			};
			this.addCompilerHook("compilation", _getCompilationHooks());
			this.addCompilerHook("thisCompilation");
		}
		else {
			this.addCompilerHook("compilation");
			this.addCompilerHook("thisCompilation");
		}
		if (this.buildOptions.compiler !== false)
		{
			this.addCompilerHook("make");
			this.addCompilerHook("finishMake");
			this.addCompilerHook("afterCompile");
			// /** @param {WebpackCompilation} compilation */(compilation) =>
			// {
			// 	// const stats = compilation.getStats();
			// 	// stats.toJson().
			// 	if (l.level >= 4)
			// 	{
			// 		const assets = compilation.getAssets();
			// 		l.write("compilation step completed, list all assets", 4, "", null, l.colors.white);
			// 		for (const asset of assets)
			// 		{
			// 			l.writeMsgTag(asset.name, "ASSET", null,  4, "   ", null, l.colors.grey);
			// 			l.value("   asset info", JSON.stringify(asset.info), 5);
			// 		}
			// 	}
			// });
			this.addCompilerHook("shouldEmit");
			this.addCompilerHook("emit");
			this.addCompilerHook("assetEmitted");
			this.addCompilerHook("afterEmit");
			this.addCompilerHook("afterDone");
			this.addCompilerHook("additionalPass");
			this.addCompilerHook("invalid");
			this.addCompilerHook("watchRun");
			this.addCompilerHook("watchClose");
		}
	}


	/**
	 * @private
	 * @param {string} hook
	 * @param {boolean} isCompilation
	 */
	writeBuildTag(hook, isCompilation)
	{
		const key = hook + this.build.wpc.name;
		if ((!this.store.data[key] || this.build.log.level >= 4) &&
			(!this.build.hasGlobalError || hookRunsWithGlobalError.includes(hook)))
		{
			const now = Date.now(),
				  l = this.build.logger,
				  activePad = l.staticPad,
				  tagTm1 = `hk::${epochToMSMS(now - this.last, true)}`,
				  tagTm2 = `rt::${epochToMSMS(now - this.start, true)}`,
				  tagHk = (!isCompilation? "compiler" : "compilation") + " hooks";
				  // tagHk = l.tag(`${!isCompilation? "compiler" : "compilation"} hook`);
			l.staticPad = "";
			this.store.data[key] = true;
			// l.valuestar(`${tagHk} ${hook}`, hook, 1, "", 1, null, [ tagTm1, tagTm2 ]);
			// l.valuestar(tagHk, hook, 1, "", 1, null, [ tagTm1, tagTm2 ]);
			const icon = withColor(l.icons.star, l.color);
			l.write(
				`${icon} execute ${tagHk}: ${hook.toLowerCase()} ${icon}`,
				1, "", null, null, false, null, [ tagTm1, tagTm2 ]
			);
			l.staticPad = activePad;
			this.last = Date.now();
		}
	};
}


module.exports = WpwHooksAnalyzePlugin.create;