/**
* @file plugin/exec.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman\
*//** */
const { breakProp } = require("./utils");
// const { GLOBAL_EVENT_BUILD_ERROR_ARGS } = require("./constants");
const { SpmhMessageUtils } = require("@spmhome/log-utils");
const { isString, isError, isPromise, merge } = require("@spmhome/type-utils");
class WpwHookHandler
{
/**
* @private
* @type {boolean|undefined}
*/
async;
/**
* @private
* @type {WpwBuild}
*/
build;
/**
* @private
* @type {number}
*/
errCt = 0;
/**
* @type {SpmhMessageCode}
*/
errCodeSkip = SpmhMessageUtils.Code.ERROR_SKIP_ON_GLOBAL_ERROR;
/**
* @private
* @type {string}
*/
hook;
/**
* @private
* @type {WpwLogger}
*/
logger;
/**
* @private
* @type {string}
*/
name;
/**
* @private
* @type {WpwModule}
*/
plugin;
/**
* @private
* @type {WpwPluginRegisteredTapOptions<any, any, boolean>}
*/
options;
/**
* @param {string} name
* @param {boolean} async
* @param {WpwModule} plugin
* @param {WpwPluginBaseTapOptions<any, any, boolean>} options
*/
constructor(name, async, plugin, options)
{
const o = options,
hookDsc = breakProp(name, plugin);
this.name = name;
this.async = async;
this.plugin = plugin;
this.build = plugin.build;
this.logger = plugin.build.logger;
this.hook = (o.hook !== "compilation" ? o.hook : (o.hookCompilation || o.hook)).toLowerCase();
if (this.hook === "processassets") {
this.hook += `:${o.stage.toLowerCase()}`;
}
o.messageStart ||= hookDsc;
o.messageDone ||= o.messageStart;
o.messageFail ||= `failed to ${o.messageStart}`;
plugin.hookMessages[this.hook] = {
start: o.messageStart, done: o.messageDone, fail: o.messageFail
};
this.options = merge({
messageDone: o.messageDone, messageFail: o.messageFail, messageStart: o.messageStart
}, options);
}
/**
* @private
* @param {string[]} xTags
* @param {Error | void} [errorOrBail]
* @returns {Error | void}
*/
done(xTags, errorOrBail)
{
const b = this.build,
l = this.logger,
p = this.plugin,
isErrOrBail = isError(errorOrBail),
errCt = b.errors.length,
gErrCt = b.errorsGlobal.length;
l.staticPad = "";
if (!isErrOrBail && errCt === this.errCt && gErrCt === 0)
{
const restart = [ ...b.info, ...b.warnings ].find((i) => i.code === SpmhMessageUtils.Code.INFO_RESTART_REQUIRED);
if (restart)
{ l.writeMsgTag(
this.options.messageDone, "RESTART REQUIRED", 1, "", xTags, false, null, null, l.icons.success
);
errorOrBail = restart;
}
else {
l.success(this.options.messageDone, this.options.logLevel || 1, "", undefined, xTags);
}
}
else
{
const skipErrIdx = b.errors.findIndex((e) => e.code === this.errCodeSkip);
if (errCt > this.errCt && skipErrIdx === -1)
{
l.fail(this.options.messageFail, 1, "", true, [ "errors", `build::${errCt}`, `global::${gErrCt}` ]);
// b.global.globalEvent.emitter.emit(GLOBAL_EVENT_BUILD_ERROR_ARGS[0], GLOBAL_EVENT_BUILD_ERROR_ARGS[1]);
}
else
{ xTags.unshift(l.tag("failed build", "error", "white"));
if (skipErrIdx !== -1) {
b.errors.splice(skipErrIdx, 1);
}
l.success(
this.options.messageDone, this.options.logLevel || 1, "", undefined, xTags, !this.options.forceRun
);
}
}
p.hookCurrent = "";
return errorOrBail || void undefined;
}
/**
* @private
* @param {string[]} xTags
* @param {Error} err
* @returns {Error | void}
*/
doneError(xTags, err)
{
if (this.build.errorCount === 0) // || !isWpwMsgUtils)
{
if (!SpmhMessageUtils.isSpmh(err))
{
this.plugin.addMessage({
// exception: !isWpwMsgUtils ? err : undefined,
exception: err,
code: SpmhMessageUtils.Code.ERROR_PLUGIN_HOOK_FAILED,
message: "an exception was thrown by hook handler: " + this.name
}, true);
}
else {
this.build.errors.push(err);
}
}
return this.done(xTags, err);
}
wrap()
{ return (/** @type {any} */...args) =>
{
const l = this.logger, options = this.options,
hookTag = "hook::" + (options.hookCompilation || options.hook) + (options.stage ?
`|${options.stage.toLowerCase().replace("process_assets_stage_", "")}` : ""),
xTags = [ `${this.plugin.type}::${this.plugin.optionsKey}`, hookTag.toLowerCase() ];
this.plugin.hookCurrent = this.hook; // this.name;
this.errCt = this.build.errors.length;
this.gErrCt = this.build.errorsGlobal.length;
if (this.build.errorsGlobal.length === 0 || options.forceRun)
{
let result;
const callback = isString(options.callback) ?
this.plugin[options.callback].bind(this.plugin) : options.callback.bind(this.plugin);
l.staticPad = "";
l.start(options.messageStart, 1, null, xTags);
l.staticPad = " ";
try {
result = callback.call(this.plugin, ...args);
}
catch (e) { return this.doneError(xTags, e); }
if (isPromise(result))
{ return /** @type {Promise<Error | void>} */(new Promise((ok, fail) =>
{ result.then((r) =>
{ const isErr = isError(r);
if (!isErr && this.build.errorCount === this.errCt)
{
ok(this.done(xTags));
}
else
{ fail(this.doneError(xTags, isErr ?
(SpmhMessageUtils.isSpmh(r) ? (r.exception || r) : r) : this.build.errors[this.errCt])
);
}
}).catch((e) => fail(this.doneError(xTags, e)));
}));
}
if (this.build.errorCount === this.errCt && !isError(result)) {
return this.done(xTags);
}
else if (isError(result)) {
return this.doneError(xTags, result);
}
else {
return this.doneError(xTags, this.build.errors[this.errCt]);
}
}
return !this.async ? this.done(xTags) : Promise.resolve(this.done(xTags));
};
}
}
// /** @type {WpwScopedHookWrapper} */
/**
* @template {boolean} A
* @template {WpwPluginHookWrapper<A>} R
* @param {string} name
* @param {A} async
* @param {WpwModule} plugin
* @param {WpwPluginBaseTapOptions<any, any, A>} options
* @returns {R}
*/
const wrapHookHandler = (name, async, plugin, options) =>
/** @type {R} */(/** @type {unknown} */((new WpwHookHandler(name, async, plugin, options)).wrap()));
module.exports = { wrapHookHandler };