utils_utils.js

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