utils_hook.js

/**
 * @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 };