/**
* @file utils/utils.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const { asArray } = require("@spmhome/type-utils");
const { isDevExec, isDevDistExec } = require("./global");
const {
existsSync, nodejsModulesGlobalPath, findFilesSync, resolvePath, __cwd, __dir, findDotJsonFileUpSync, joinPath,
deleteFileSync,
deleteDirSync,
dirname
} = require("@spmhome/cmn-utils");
const {
isWpwBuildOptionsPluginKey, isWpwBuildOptionsGroupKey, isWpwBuildOptionsExportKey, WpwBuildOptionsPluginKeys,
WpwBuildOptionsExportKeys, WpwBuildOptionsGroupKeys
} = require("../types/constants");
/**
* @since 1.11.0
* @returns {WpwBuildOptionsKey[]}
*/
const wpwBuildOptionsKeys = [ ...WpwBuildOptionsPluginKeys, ...WpwBuildOptionsExportKeys, ...WpwBuildOptionsGroupKeys ];
/**
* @since 1.11.0
* @returns {WpwBuildOptionsRootKey[]}
*/
const wpwBuildOptionsRootKeys = [ ...WpwBuildOptionsPluginKeys, ...WpwBuildOptionsExportKeys ];
/**
* Break property name into separate spaced words at each camel cased character
*
* @param {string} prop
* @param {WpwModule} wpwMod
* @returns {string} string
*/
const breakProp = (prop, wpwMod) =>
{
const bNames = wpwMod.build.wrapper.builds.map((b) => b.name).join("|"),
rgxBuilds = new RegExp(` (${bNames}) (${bNames})`, "g");
return prop.replace(/_/g, "")
.replace(/Builds\[/g, "Builds [")
.replace(/Build?\[/g, "Build [")
.replace(/[A-Z]{2,}/g, (v) => v[0] + v.substring(1).toLowerCase())
.replace(/[a-z][A-Z]/g, (v) => `${v[0]} ${v[1]}`).toLowerCase()
.replace(rgxBuilds, (_, g1, g2) => ` ${g1.toLowerCase()} & ${g2.toLowerCase()}`);
};
/**
* @param {WpwBuild} build
* @param {WpwLogger} [logger]
*/
const cleanupBuildDone = (build, logger) =>
{
const b = build, l = logger || b.logger || b.wrapper?.logger || console;
let tmpPath = joinPath(b.getTempPath(), b.name);
try
{ if (existsSync(tmpPath))
{ l.log(" delete temporary build directory");
deleteDirSync(tmpPath);
}
tmpPath = dirname(tmpPath);
if (existsSync(tmpPath) && findFilesSync("*", { cwd: tmpPath }).length === 0)
{ l.log(" delete empty base temporary directory");
deleteDirSync(tmpPath);
}
} catch(e) { l.error(e, " "); }
if (b.virtualEntry)
{ const vFile = `${b.virtualEntry.filePathAbs}${b.source.dotext}`;
if (existsSync(vFile))
{ l.log(" delete virtual entry file");
try { deleteFileSync(vFile); }
catch(e) { l.error(e, " "); }
}
}
if (b.options?.vsceiso?.enabled)
{ try
{ findFilesSync("**/*{.vsix,-lock.json}", { cwd: b.options.vsceiso.dist || b.paths.dist })
.forEach((f) => { l.log(" delete vsix / package lock file"); deleteFileSync(f); });
} catch(e) { l.error(e, " "); }
}
if (!b.tsc || !b.source?.info)
{ const files = findFilesSync("*.tmp", { absolute: true });
for (const file of files)
{ l.log(` delete runtime tmp tsconfig file @ '${b.tsc.configFileRtPath}'`);
try { deleteFileSync(file); }
catch(e) { l.error(e, " "); }
}
}
else if (b.tsc?.configFileRtIsTemp)
{ // if (/^.+[\\/]\.wpw\.[abc]+[0-9]{1,2}\.tsc(?:\..+?)?\.(?:tmp|json)$/.test(b.source.info.path))
if (/^.*\.wpw.+\.tmp$/.test(b.tsc.configFileRtPath))
{ l.log(` delete runtime tmp tsconfig file @ '${b.tsc.configFileRtPath}'`);
try {
deleteFileSync(b.tsc.configFileRtPath);
} catch(e) { l.error(e, " "); }
}
}
try
{ // l.log(" dispose build instance");
// b.dispose();
if (findFilesSync("**/*", { cwd: b.paths.dist }).length === 0) // delete basedir if empty
{ l.log(" delete empty dist directory");
deleteDirSync(b.paths.dist);
}
} catch(e) { l.error(e, " ");}
};
/**
* @param {string} dir
* @param {WpwSourceCodeExtension | WpwSourceDotExtensionApp} ext
* @param {boolean | undefined} [recurse]
* @param {string | string[] | undefined} [ignore]
* @returns {any}
*/
const createEntryObjFromDir = (dir, ext, recurse, ignore) =>
{
const pattern = !recurse ? `*${ext}` : `**/*${ext}`;
if (!ext.startsWith(".")) {
ext = /** @type {WpwSourceDotExtensionApp} */("." + ext);
}
return findFilesSync(
pattern, {
absolute: false, cwd: dir, dotRelative: false, posix: true, maxDepth: !recurse ? 1 : undefined, ignore
}
).reduce((obj, e)=> { obj[e.replace(ext, "")] = `./${e}`; return obj; }, {});
};
/**
* @param {Error | WebpackError} err
* @param {string} topTitle
* @returns {string}
*/
const formatJsError = (err, topTitle = "EXCEPTION") =>
{
let msg = `italic(${topTitle}):\n ${err.message}\n italic(STACK TRACE):\n` +
err.stack?.replace(/([a-z]+?)Error: .+\n {0,2}/i, "")
.replace(/([a-z]+?)Error: .+\n/gi, "")
.replace(/\nat /g, "\n at ");
// eslint-disable-next-line ts/dot-notation
const detail = err["detail"];
if(detail && err.name.includes("Webpack")) {
msg += `\n italic(DETAIL):\n${detail}`;
}
return msg.trim();
};
/**
* @param {WpwPluginConfigScript | WpwPluginConfigRunScripts} cfg
* @param {string[]} addtl
* @returns {string[]}
*/
const getScriptBuildAssets = (cfg, ...addtl) =>
{
const assets = cfg.assets || { immutable: false };
return cfg.items.map((i) => asArray(i.paths?.output)).filter((p) => !!p).flat()
.concat(asArray(assets.paths)).concat(addtl);
};
/**
* @param {WpwBuild} build
* @param {WpwPluginConfigScript | WpwPluginConfigRunScripts} cfg
* @param {string[]} addtl
* @returns {string[]}
*/
const getScriptBuildDependencies = (build, cfg, ...addtl) =>
{
const basePath = build.getBasePath();
return cfg.items.map((i) => asArray(i.paths?.input)).filter((p) => !!p).flat()
.concat(
cfg.items.map((i) => i.command.split(" ")
.find((p) => /[\\w\\/\\.-]+?\\.[a-zA-Z0-9]{2,6}/.test(p)))
.filter((p) => p && existsSync(resolvePath(basePath, p)))
).concat(addtl);
};
/**
* @since 1.11.0
* @param {WpwBuild | null} [build]
* @param {boolean | null} [prependBuildNm]
* @param {string} [xKey]
* @param {"-" | "_"|""} [jChr]
* @returns {string}
*/
const getUniqBuildKey = (build, prependBuildNm, xKey, jChr = "-") =>
{
return (prependBuildNm ? `${build.name}${jChr}` : "") + `${asArray(build.target).join(jChr)}${jChr}${build.mode}` +
(build.library ? `${jChr}${build.library}` : "") + (xKey ? `${jChr}${xKey}` : "");
};
/**
* @since 1.11.0
* @param {WpwWrapper} wpw
* @param {string} xKey
* @param {"-" | "_"|""} [jChr]
* @returns {string}
*/
const getUniqKey = (wpw, xKey, jChr = "-") => `wpw${jChr}${wpw.mode}` + (xKey ? `${jChr}${xKey}` : "");
/**
* @since 1.11.0
* @param {string} group
* @param {any} key
* @param {boolean} stat set to `true` to check to see if the options section specified
* by 'key' exists in the current configuration, adn return false if not, as opposed to only
* checking to see if 'key' is an actual/valid options section key
* @type {SpmhTypeValidationResult<WpwBuildOptionsKey>}
*/
const isWpwBuildOptionsKey = (group, key, stat, options) =>
{
return !!key && wpwBuildOptionsKeys.includes(key) &&
(isWpwBuildOptionsExportKey(key) || isWpwBuildOptionsPluginKey(key) || isWpwBuildOptionsGroupKey(key)) &&
(!stat || !!(group ? options[group] && options[group][key] : options[key]));
};
/**
* @since 1.11.0
* @param {any} key
* @param {boolean} stat set to `true` to check to see if the options section specified
* by 'key' exists in the current configuration, adn return false if not, as opposed to only
* checking to see if 'key' is an actual/valid options section key
* @type {SpmhTypeValidationResult<WpwBuildOptionsRootKey>}
*/
const isWpwBuildOptionsRootKey = (key, stat, options) =>
!!key && wpwBuildOptionsRootKeys.includes(key) && isWpwBuildOptionsKey(null, key, stat, options);
/**
* @since 1.6.0
* Get base wpw path, i.e. '/nodes_modules/@spmhome/webpack-wrap' whether it is installed
* locally or globally, except for the wpw project itself. FOr the wpw project itself, the 'wpw'
* path is just the project 'base' path
*/
/**
* @returns {string}
*/
const wpwPath = () =>
{
if (isDevExec) {
return resolvePath(__cwd, ".");
}
if (isDevDistExec) {
return resolvePath(__cwd, "dist");
}
let depPath = resolvePath(__cwd, "node_modules/@spmhome/webpack-wrap");
if (!existsSync(depPath)) {
depPath = resolvePath(nodejsModulesGlobalPath(), "@spmhome/webpack-wrap");
}
return depPath;
};
/**
* @since 1.12.0
* @returns {string}
*/
const wpwNodeModulesPath = () => joinPath(wpwPath(), "node_modules");
const wpwVersion = () =>
{
const pkg = findDotJsonFileUpSync("package.json", __dir);
return pkg.data?.version || "???";
};
module.exports = {
breakProp, createEntryObjFromDir, formatJsError, getUniqBuildKey, getUniqKey, isWpwBuildOptionsKey,
isWpwBuildOptionsRootKey, wpwBuildOptionsKeys, wpwBuildOptionsRootKeys, wpwPath, wpwVersion,
wpwNodeModulesPath, getScriptBuildAssets, getScriptBuildDependencies, cleanupBuildDone
};