utils_print.js

/**
 * @file src/utils/print.js
 * @copyright 2025 SPMHOME, LLC
 * @author Scott Meesseman @spmeesseman
 *//** */

const { printBanner, SpmhCliLogger } = require("@spmhome/log-utils");
const { existsSync, execIf } = require("@spmhome/cmn-utils");
const { withColor, lightenColor } = require("@spmhome/log-utils");
const { wpwVersion, getUniqBuildKey, getUniqKey } = require("./utils");
const { WpwBuildOptionsPluginKeys, WpwBuildOptionsExportKeys, WpwBuildOptionsGroupKeys } = require("../types/constants");
const { pickNot, isFunction, isString, asArray, isObject, stripAnsiColorCodes, safeStringify } = require("@spmhome/type-utils");


/**
 * @param {WpwBuild} b
 * @param {string} txt
 * @param {SpmhMessage[]} q
 * @param {boolean} failed
 * @param {boolean} last
 */
const _printQ = (b, txt, q, failed, last) =>
{
    const l = b.logger;
    execIf(!!l && q.length > 0, () =>
    {
        const icon = withColor(!failed ? "✔" : "✘", l.color),
              sep = l.sep(1, null, l.color, true),
              tagTotal = l.tag(`${q.length}`, l.color, "white", false, false, null, [ "(", ")" ]);
        l.sep(1, null, l.color);
        q.forEach((msg, i) =>
        {   let m, xMsg = msg.xMessage.toString();
            const statIcon = msg.isErrorType ? l.icons.error : (msg.isWarningType ? l.icons.warn : l.icons.info),
                  statTag = l.tag(statIcon, l.color, "white"),
                  sPad = "".padEnd(SpmhCliLogger.envTagLen + 3),
                  pTag = l.tag(msg.code) + l.tag(b.name.toUpperCase()),
                  pPad = ((m = xMsg.split("\n", 2)[1].match(/^( +)/)) !== null) ? m[1] : "",
                  tagCurrent = l.tag(`${i + 1}`, l.color, "white", false, false, false, [ "(", ")" ]),
                  tagNameNum = withColor(`${txt.toUpperCase()} #`, "lightgrey"),
                  mHdr = `${statTag} ${tagNameNum} ${tagCurrent} ${withColor("of", "lightgrey")} ${tagTotal}`,
                  tPad = l.maxLine - msg.code.length - 2 - b.name.length - 2 - stripAnsiColorCodes(mHdr).length - 1;
            // xMsg = xMsg.replace(new RegExp(`^ {${SpmhNodeCliLogger.envTagLen + 2}}`, "gm"), "");
            // const pPad2 = "".padEnd(SpmhNodeCliLogger.envTagLen - 2);
            xMsg = `${sPad}${mHdr}${"".padEnd(tPad)}${pTag}\n` +
                   `${pPad}${sep}\n${pPad}${xMsg}${i <= q.length - (!last ? 1 : 2) ? `\n${pPad}${sep}` : ""}`;
            //     `${pPad}${sep}\n${pPad2}${xMsg}${i <= q.length - (!last ? 1 : 2) ? `\n${pPad}${sep}` : ""}`;
            msg.xMessage.setXMessage(xMsg, true);
            // l.write(msg, 1, "", icon);
            l.write(msg, 1, "", icon, null, false, null, undefined, null, null, null, null, true);
        });
    });
};


/**
 * @param {WpwBuild} build
 * @param {boolean} failed
 * @param {string[]} suppress
 */
const printBuildMessages = (build,failed, suppress = []) =>
{
    const b = build, ign = asArray(suppress),
          err = b.errors.filter((i, idx1) => b.errors.every((m, idx2) => idx1 === idx2 || !i.isEqual(m, true))),
          wrn = b.warnings.filter(
              (i, idx1) => !ign.includes(i.code) && b.warnings.every((m, idx2) => idx1 === idx2 || !i.isEqual(m))
          ),
          info = b.info.filter(
              (i, idx1) => !ign.includes(i.code) && b.info.every((m, idx2) => idx1 === idx2 || !i.isEqual(m))
          );
    _printQ(build, "message", info, failed, err.length === 0 && wrn.length === 0);
    _printQ(build,"warning", wrn, failed, err.length === 0);
    _printQ(build, "error", err, failed, true);
};


/**
 * @param {WpwBuild} build
 * @param {any} logIcon
 * @param {string} lPad
 */
const printBuildProperties = (build, logIcon, lPad) =>
{
    const b = build, l = build.logger;
    execIf(!!l && !!b.tsc?.tsconfig, (_, c) =>
    {
        l.sep(1, logIcon);
        l.write("details", 1, lPad, logIcon, c);
        l.value("   name", b.name, 1, lPad, logIcon);
        l.value("   version", b.pkgJson.version, 1, lPad, logIcon);
        l.value("   type", b.type, 1, lPad, logIcon);
        l.value("   target platform", asArray(b.target).join(" | "), 1, lPad, logIcon);
        l.value("   library type", b.library, 1, lPad, logIcon);
        l.value("   environment mode", b.mode, 1, lPad, logIcon);
        l.value("   source language", b.sourceInfo?.language, 2, lPad, logIcon);

        if (l.level >= 2 && b.source.language)
        {
            l.value("   pkg.json.name", b.pkgJson.name, 2, lPad, logIcon);
            l.value("   pkg.json.scoped.name", b.pkgJson.scopedName.name, 2, lPad, logIcon);
            l.value("   pkg.json.scoped.scope", b.pkgJson.scopedName.scope, 2, lPad, logIcon);
            l.value("   # of tsc files found", asArray(b.tsc.tsconfig.files).length, 3, lPad, logIcon);
            l.value("   source code type", b.source.language, 2, lPad, logIcon);
            l.value("   log configuration", safeStringify(b.log), 3, lPad, logIcon);
        }

        if (existsSync(build.statsfile))
        {   try
            {   const store = build.stats,
                    xKey = build.buildCount.toString() + (!build.isRebuild ? "" : "_rebuild");
                let buildTm = store[getUniqBuildKey(build, true, xKey, "_")] || {};
                l.sep(1, logIcon);
                l.write("build statistics", 1, lPad, logIcon, c);
                l.value("   count", buildTm.count || "not found", 1, lPad, logIcon);
                l.value("   average", buildTm.average ? buildTm?.averageFmt : "not found", 1, lPad, logIcon);
                l.value("   fastest", buildTm.fastest ? buildTm?.fastestFmt : "not found", 1, lPad, logIcon);
                l.value("   slowest", buildTm.slowest ? buildTm?.slowestFmt : "not found", 1, lPad, logIcon);
                buildTm = store[getUniqKey(build.wrapper, xKey, "_")] || {};
                l.sep(1, logIcon);
                l.write("rebuild statistics", 1, lPad, logIcon, c);
                l.value("   count", buildTm.count || "not found", 1, lPad, logIcon);
                l.value("   average", buildTm.average ? buildTm.averageFmt : "not found", 1, lPad, logIcon);
                l.value("   fastest", buildTm.fastest ? buildTm.fastestFmt : "not found", 1, lPad, logIcon);
                l.value("   slowest", buildTm.slowest ? buildTm.slowestFmt : "not found", 1, lPad, logIcon);
            } catch {}
        }

        l.sep(1, logIcon);
        l.write("paths", 1, lPad, logIcon, c);
        l.value("   base/project directory", b.getBasePath(), 1, lPad, logIcon);
        l.value("   context directory", b.getContextPath(), 1, lPad, logIcon);
        l.value("   distribution directory", b.getDistPath(), 1, lPad, logIcon);
        l.value("   source directory", b.getSrcPath(), 2, lPad, logIcon);
        l.value("   temp directory", b.getTempPath(), 2, lPad, logIcon);
        l.sep(1, logIcon);
        l.write("build export and plugin options", 1, lPad, logIcon, c);

        [ ...WpwBuildOptionsPluginKeys, ...WpwBuildOptionsExportKeys ].sort().forEach((pk) =>
        {
            const p = /** @type {Record<string, any>} */(b.options[pk] || {});
            l.write(pk, 1, 3, logIcon, lightenColor(c));
            const keys = Object.keys(b.options[pk] || {});
            if (keys.length === 0) { l.value("enabled", false, 1, 6, logIcon); }
            keys.sort(sortBuildOptions).forEach((gk) => { l.value(gk, p[gk], 1, 6, logIcon); });
        });

        WpwBuildOptionsGroupKeys.sort().forEach((pk) =>
        {
            const clr = "floralwhite";
            l.write(`${pk} [group]`, 1, 3, logIcon, clr);
            const keys = Object.keys(b.options[pk] || {});
            if (keys.length === 0)
            {
                l.value("enabled", false, 1, 6, logIcon);
            }
            else
            {   l.value("enabled", true, 1, 6, logIcon);
                keys.filter((gk) => gk !== "enabled").sort(sortBuildOptions).forEach((gk) =>
                {
                    const p = /** @type {Record<string, any>} */(b.options[pk] || {});
                    l.write(gk, 1, 6, logIcon, clr);
                    const keys = Object.keys(p[gk] || {});
                    if (keys.length === 0)
                    {
                        l.value("enabled", false, 1, 9, logIcon);
                    }
                    else {
                        keys.sort(sortBuildOptions).forEach((ck) => l.value(ck,p[gk][ck],1, 9, logIcon));
                    }
                });
            }
        });

        if (l.level >= 3)
        {
            l.sep(3, logIcon);
            l.write(`paths relative to [${b.getBasePath()}]:`, 3, lPad, logIcon, c);
            l.value("   context directory", b.getContextPath({ rel: true }), 3, lPad, logIcon);
            l.value("   distribution directory", b.getDistPath({ rel: true }), 3, lPad, logIcon);
            l.value("   source directory", b.getSrcPath({ rel: true }), 3, lPad, logIcon);
        }

        l.sep(1, logIcon);
        l.write("source config", 1, lPad, logIcon, c);
        l.value("   source code ext", b.source.ext, 1, lPad, logIcon);
        l.value("   source code type", b.source.language, 1, lPad, logIcon);
        if (b.source.info)
        {
            l.value("   config file", b.source.info.file, 1, lPad, logIcon);
            if (l.level >= 2)
            {
                l.value("   config directory", b.source.info.dir, 2, lPad, logIcon);
                l.value("   config path", b.source.info.path, 2, lPad, logIcon);
                l.value("   config file info", safeStringify(b.source.info), 2, lPad, logIcon);
                if (b.tsc.tsconfig)
                {
                    if (l.level === 3)
                    {   l.value("   ts options",
                            safeStringify(pickNot(b.tsc.tsconfig, "compilerOptions", "files")
                        ), 3, lPad, logIcon);
                    }
                    else if (l.level >= 4) {
                        l.value("   ts compiler opts", safeStringify(b.tsc.tsconfig.compilerOptions), 4, lPad, logIcon);
                        l.value("   ts auto-gen files list", safeStringify(b.tsc.tsconfig.files), 4, lPad, logIcon);
                    }
                }
            }
        }
        l.sep(1, logIcon);
        printWpcProperties(build, undefined, lPad);
    }, undefined, undefined, l.color);
};


/**
 * @param {WpwBuild} build
 * @param {any} logIcon
 * @param {string} lPad
 */
const printWpcProperties = (build, logIcon, lPad) =>
{
    const b = build, l = b.logger;
    execIf(!!l && !!b.wpc.mode, (_, c) =>
    {
        l.write("webpack export config", 1, lPad, logIcon, l.color);
        l.value("   name", b.wpc.name, 1, lPad, logIcon);
        l.value("   build mode", b.wpc.mode, 1, lPad, logIcon);
        l.value("   target environment",b.wpc.target, 1, lPad, logIcon);
        l.value("   logging level", b.wpc.infrastructureLogging?.level || "none", 2, lPad, logIcon);
        l.value("   context directory", b.wpc.context, 1, lPad, logIcon);
        l.value("   output directory", b.wpc.output.path, 1, lPad, logIcon);
        l.value("   cache", safeStringify(b.wpc.cache), 3, lPad, logIcon);
        l.value("   devtool", safeStringify(b.wpc.devtool), 3, lPad, logIcon);
        l.value("   entry", safeStringify(b.wpc.entry), 2, lPad, logIcon);
        l.value("   experiments", safeStringify(b.wpc.experiments), 2, lPad, logIcon);
        if (b.wpc.externals && isFunction(b.wpc.externals)) {
            l.value("   externals", "[function()]", 2, lPad, logIcon);
        } else {l.value("   externals", safeStringify(b.wpc.externals), 2, lPad, logIcon);  }
        l.value("   optimization", safeStringify(b.wpc.optimization), 2, lPad, logIcon);
        l.value("   output", safeStringify(b.wpc.output), 2, lPad, logIcon);
        l.value("   resolve", safeStringify(b.wpc.resolve), 2, lPad, logIcon);
        l.value("   resolve loader", safeStringify(b.wpc.resolveLoader), 2, lPad, logIcon);
        l.value("   plugins", asArray(b.wpc.plugins).map((p) => p.optionsKey).join(" | "), 2, lPad, logIcon);
        asArray(b.wpc.module?.rules).forEach((r, i) =>
        {
            l.write("   rule " + (i + 1 + ":"), 1, lPad, logIcon);
            // @ts-ignore
            const loader = r.loader || (r.use && !isString(r.use) ? r.use.loader : "");
            if (loader) {
                l.value("      loader ", loader, 1, lPad, logIcon);
            }
            asArray(r.test).forEach((t) => {
                l.value("      test", !isObject(t) ?  t.toString() : JSON.stringify(t), 1, lPad, logIcon);
            });
            asArray(r.include).forEach((i) => {
                l.value("      include", !isObject(i) ?  i.toString() : JSON.stringify(i), 1, lPad, logIcon);
            });
            asArray(r.exclude).forEach((e) => {
                l.value("      exclude", !isObject(e) ?  e.toString() : JSON.stringify(e), 1, lPad, logIcon);
            });
            // @ts-ignore
            const options = r.options || (r.use && !isString(r.use) ? r.use.options : undefined);
            if (options)
            {
                l.write("      options:", 2, lPad, logIcon);
                Object.entries(options).filter((e) => e[0] !== "implementation").forEach((e) => {
                    l.value("         " + e[0], e[1], 2, lPad, logIcon);
                });
            }
        });
        l.value("   stats", JSON.stringify(b.wpc.stats), 3, lPad, logIcon);
        l.sep(1, logIcon);
    }, undefined, undefined, l.color);
};


/**
 * @param {WpwWrapper} w
 * @param {any} logIcon
 * @param {string} lPad
 */
const printWpwProperties = (w, logIcon, lPad) =>
{
    const l = w.logger;
    execIf(!!l, (_, c) =>
    {   const bannerPad = lPad.padEnd(Math.floor(l.preMsgTagLen / 2), " ");
        printBanner(w.pkgJson.name, w.pkgJson.version, true, true, true, 120, "\n", bannerPad);
        l.write("cli arguments", 1, lPad, logIcon, c);
        l.value("   argv", process.argv.slice(2).join(" "), 1, lPad);
        l.value("   args", safeStringify(w.cli), 1, lPad);
        l.write("global configuration", 1, lPad, logIcon, c);
        Object.keys(w.global).filter(k => typeof w.global[k] !== "object")
                            .forEach((k) => l.value(`   ${k}`, w.global[k], 1, lPad, logIcon));
        l.write("wpw details", 1, lPad, logIcon, c);
        l.values([
            [ "package version", wpwVersion() ], [ "schema version", w.schemaVersion ],
            [ "default mode", w.mode ], [ "cache directory", w.cacheDir ],
            [ "stats file", w.statsfile ], [ "# of defined builds", w.buildConfigs.length ],
            [ "defined build names", w.buildConfigs.map(b => `${b.name}::[${b.type}]`).join(" | ")  ]
        ], 2, lPad + "   ", false, null, logIcon);
        if (w.stats)
        {   try
            {   const buildTm = w.stats.all || {};
                l.write("multi-build statistics", 1, lPad, logIcon, c);
                l.value("   count", buildTm.count || "not found", 1, lPad, logIcon);
                l.value("   average", buildTm.average ? buildTm?.averageFmt : "not found", 1, lPad, logIcon);
                l.value("   fastest", buildTm.fastest ? buildTm?.fastestFmt : "not found", 1, lPad, logIcon);
                l.value("   slowest", buildTm.slowest ? buildTm?.slowestFmt : "not found", 1, lPad, logIcon);
            } catch {}
        }
    }, undefined, undefined, l.color);
};


/**
 * @param {string} x
 * @param {string} y
 * @returns {number}
 */
const sortBuildOptions = (x, y) => (
    x === "enabled" || x === "disabled" ? -1 :
    (y === "enabled" || y === "disabled" ? 1 : x < y ? -1 : (x > y ? 1 : 0))
);


module.exports = { printBuildMessages, printBuildProperties, printWpwProperties };