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