core_options.js

/**
 * @file services/options.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 *//** */

const { SpmhMessageUtils } = require("@spmhome/log-utils");
const { isWpwBuildOptionsRootKey } = require("../utils/utils");
const { isWpwBuildOptionsGroupKey, isWebpackLibrary } = require("../utils");
const { existsSync, findExPathSync, absPath } = require("@spmhome/cmn-utils");
const { isObject, mergeIfStrong, fromDotPath, pluralize, apply, merge, applyIf, mergeIf } = require("@spmhome/type-utils");


/**
 * @class WpwBuildOptionsService
 */
class WpwBuildOptionsFinalizer
{
    static _isoReleaseActive = false;

    /**
     * @private
     * @type {string | number}
     */
    _lPad;
    /**
     * @private
     * @type {WpwBuild}
     */
    _build;


    /**
     * @static
     * @param {WpwBuild} build
     * @param {string | number} lPad
     */
    constructor(build, lPad)
    {
        this._lPad = lPad;
        this._build = build;
    }


    configure()
    {
        const results = [], build = this._build;
        build.library ||= this.getLibraryType(build);
        build.logger.write("auto-configure build options dependencies", 1, this._lPad);
        if (build.autoConfig !== false)
        {
            this.configureOptionsByCmdLine(build, results);
            this.configureOptionsByMode(build, results);
            this.configureOptionsByType(build, results);
            this.configureOptionsByPkgType(build, results);
            this.configureOptionsByOptions(build, results);
            this.configureOptionsByLanguage(build, results);
            this.configureOptionsCommon(build, results);
            this.validateOptions(build);
            if (results.length > 0)
            {   const resultTxt = pluralize("option", results.length);
                build.addMessage({
                    lLvl: 3,  module: "main", lSkip: true, code: SpmhMessageUtils.Code.INFO_AUTO_ENABLED_OPTION,
                    message: `total of ${results.length} configuration ${resultTxt} auto-enabled or modified at runtime`,
                    detail: `modified configuration ${resultTxt}: ${results.splice(0).join(" | ")}'`
                });
            }
        }
        else {
            build.logger.write("   auto-configuration is disabled", 1, this._lPad);
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureIsoRelease(build, results)
    {   //
        // TODO - processing of additional builds for iso release
        //        what to do for app / webapp combo?
        //
        if (WpwBuildOptionsFinalizer._isoReleaseActive) { return; }

        const isVsCode = build.pkgJson.engines?.vscode,
        //       o = build.options,
              iso = !isVsCode || build.isAnyLib ? "npmiso" : "vsceiso";
        //       isMainBuild = !build.isMultiTarget ||
        //             ((build.type === "app" || build.type === "lib") &&
        //              (!o.output || !o.output.name || o.output.name === build.name)) ||
        //              (build.type === "app" && o.output.name.endsWith(build.name)),
        //       mainCfg = isMainBuild ? build :
        //         build.buildConfigs.find((b) =>
        //             b.options[iso]?.enabled && (b.type === "app" || b.type === "lib") &&
        //             (!b.options.output?.name || b.options.output.name === b.name)
        //         ) ||
        //         build.buildConfigs.find((b) =>
        //             b.options[iso]?.enabled &&
        //             (b.type === "app" && b.options.output?.name && b.options.output.name.endsWith(b.name))
        //         );

        // if (!isMainBuild && build.builds.find((b) => b.name === mainCfg.name)) {
        //     return;
        // }

        // this.enableDependencyOption(build, results, null, iso);
        if (build.options[iso]?.enabled)
        {   //
            // const _ = (/** @type {string} */ t, /** @type {WpwBuildConfig} */ b) =>
            //             b.target === t && !b.disabled && b.type === build.type && build.getDistPath().endsWith(t);
            WpwBuildOptionsFinalizer._isoReleaseActive = true;
            mergeIf(
                build.options[iso],
                // isMainBuild ? {} : mainCfg.options[iso],
                build.wrapper.schema.defaults("WpwPluginConfigIsoRelease"),
                {
                    enabled: true,
                    nls: existsSync("package.nls.json"),
                    license: findExPathSync([ "LICENSE", "LICENSE.txt" ], [ ".", ".github" ], false) || false,
                    //          !!(build.options.licensefiles?.enabled),
                    changelog: findExPathSync([  "CHANGELOG", "CHANGELOG.md" ], [ ".", ".github" ], false) || false,
                    readme: findExPathSync([ "README", "README.md" ], [ ".", ".github", ".vscode" ], false) || false
                }
            );
            if (!build.options[iso].dist) // &&
            {    // !!build.buildConfigs.find((b) => _("node", b)) && !!build.buildConfigs.find((b) => _("web", b)))
                build.options[iso].dist = build.getDistPath();
            }
            else {
                build.options[iso].dist = absPath(build.options[iso].dist, false, build.getBasePath());
            }
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureOptionsCommon(build, results)
    {
        build.logger.write("   configure common options", 2, this._lPad);
        this.enableDependencyOption(build, results, null, "dispose", true);
        if (!build.options.analyze || !build.options.analyze.hooks || !build.options.analyze.hooks.enabled)
        {
            this.enableDependencyOption(build, results, "analyze", "hooks", true, "compiler");
            build.options.analyze.hooks.compilation = { default: true,  processAssets: true };
        }
        if (build.options.analyze.hooks.compiler !== false)
        {
            build.options.analyze.hooks.compiler = true;
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureOptionsByCmdLine(build, results)
    {
        build.logger.write("   configure options by cli arguments", 2, this._lPad);
        if (build.cli.analyze || build.cli.analyzex)
        {
            this.enableDependencyOption(build, results, "analyze", "analyzer", true, "open");
            if (build.cli.analyzex) {
                build.options.analyze.analyzer.browser = build.cli.analyzex;
            }
        }
        if (build.cli.clean)
        {
            this.enableDependencyOption(build, results, null, "clean", true);
        }
        if (build.logger.isWpwLogLevel(build.cli.loglevel))
        {
            build.logger.level = build.cli.loglevel;
        }
        // if (build.cli.release)
        // {
        //     this.enableDependencyOption(build, results, null, "release", true, "onCmdLineOnly");
        // }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} _results
     */
    configureOptionsByLanguage(build, _results)
    {
        build.logger.write(`   configure options by language [${build.sourceInfo.language}]`, 2, this._lPad);
        if (build.isTs)
        {   if (build.options.tscheck?.javascript)
            {
                build.options.tscheck.javascript.enabled = false;
            }
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureOptionsByMode(build, results)
    {
        build.logger.write(`   configure options by mode [${build.mode}]`, 2, this._lPad);

        this.enableDependencyOption(build, results, null, "cache");
        if (build.options.cache?.enabled)  {
            applyIf(build.options.cache, { type: "filesystem", verbose: build.logger.level >= 4 });
        }

        if (build.mode === "production")
        {
            if (build.isAnyAppOrLib)
            {
                this.enableDependencyOption(build, results, null, "banner");
                this.enableDependencyOption(build, results, null, "licensefiles");
                this.enableDependencyOption(build, results, null, "optimization", false, "minify");
                this.enableDependencyOption(build, results, null, "hash");
                if (build.options.hash?.enabled) {
                    build.options.hash.emitNoHash = build.options.hash.emitNoHash !== false && !build.options.hash.emitAppNoHash;
                }
                this.configureIsoRelease(build, results);
            }
        }
        else if (build.mode === "development")
        {
            if (build.isAnyAppOrLib)
            {
                if (build.options.hash)  {
                    build.options.hash.enabled = false;
                }
                if (!build.options.devtool) {
                    build.options.devtool = { enabled: true, type: "source-map", mode: "plugin"  };
                }
                else {
                    applyIf(build.options.devtool, { mode: "plugin", type: "source-map" });
                }
                if (build.options.devtool.mode === "plugin") {
                    this.enableDependencyOption(build, results, null, "sourcemaps");
                }
            }
        }
        else // if (build.mode === "none" / "tests")
        {
            if (build.options.hash?.enabled)
            {
                build.options.hash.enabled = false;
            }
            if (!build.options.externals?.all)
            {
                build.options.externals = apply(build.options.externals, { all: true });
            }
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureOptionsByOptions(build, results)
    {
        build.logger.write("   configure options by associated options", 2, this._lPad);
        if (build.options.runtimevars?.enabled)
        {
            if (build.isAnyLib) {
                this.enableDependencyOption(build, results, null, "hash", false, "emitNoHash");
            }
            else {
                this.enableDependencyOption(build, results, null, "hash", false);
            }
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureOptionsByPkgType(build, results)
    {
        build.logger.write(`   configure options by library type [${build.library}]`, 2, this._lPad);
        const outputLibCfg = build.options.output?.library,
              outputLib = isWebpackLibrary(outputLibCfg) ? outputLibCfg : outputLibCfg?.type,
              libWpcProps = [ build.library, outputLib, build.options.output?.libraryTarget ],
              isModule = build.isModule || libWpcProps.find((p) => /-module[\\/]/.test(p));
        if (isModule) {
            this.enableDependencyOption(build, results, null, "experiments", true);
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {any[]} results
     */
    configureOptionsByType(build, results)
    {
        build.logger.write(`   configure options by build type [${build.type}]`, 2, this._lPad);
        if (build.isAnyAppOrLib)
        {
            this.enableDependencyOption(build, results, "analyze", "circular");
            if (build.isApp)
            {
                this.enableDependencyOption(build, results, null, "shebang", true);
            }
            else if (build.isLib && !build.options.output.library)
            {
                build.options.output.library = build.library;
            }
            if (build.isAnyAppOrLib && !build.options.output.libraryTarget)
            {
                build.options.output.libraryTarget = build.library;
            }
            if (build.debug)
            {
                this.enableDependencyOption(build, results, null, "experiments", true, "layers");
            }
            if (build.isTs || build.isTsJs)
            {
                this.enableDependencyOption(build, results, null, "tscheck");
            }
            else if (build.isJs || build.isJsTs)
            {
                this.enableDependencyOption(build, results, null, "tscheck", false, "javascript.enabled");
            }
        }
        else if (build.type === "types")
        {
            this.enableDependencyOption(build, results, null, "types", true);
            if (build.options.types.mode === "tscheck")
            {
                this.enableDependencyOption(build, results, null, "tscheck", true);
            }
            else if (build.options.tscheck)
            {
                build.options.tscheck.enabled = false;
            }
            if (build.options.tscheck?.javascript)
            {
                build.options.tscheck.javascript.enabled = false;
            }
        }
        else if (!build.isTranspiled)
        {
            if (build.options.tscheck)
            {
                build.options.tscheck.enabled = false;
            }
        }
    }


    /**
     * @private
     * @template {WpwAutoEnableOptionOptionParam<G>} O
     * @template {WpwBuildOptionsGroupKey | null} G
     * @param {WpwBuild} build
     * @param {string[]} results
     * @param {G} group option name to set the `enabled` field for
     * @param {O} option option name to set the `enabled` field for
     * @param {boolean} [force] force even if wpwrc file has a different value, defaults to `false`
     * @param {...string} properties additional properties to set
     * @returns {any}
     * @throws {WpwError}
     */
    enableDependencyOption(build, results, group, option, force, ...properties)
    {
        const l = build.logger, optionFmt = `${group ? `${group}.` : ""}${option}`;
        l.write(`      auto-enable configuration option '${optionFmt}'`, 1, this._lPad);
        l.value("         force", force, 4, this._lPad);
        l.value("         properties", properties.join(", ") || "n/a", 4, this._lPad);

        if (group)
        {   if (!isWpwBuildOptionsGroupKey(group))
            {   throw build.addMessage({
                    code: SpmhMessageUtils.Code.ERROR_SHITTY_PROGRAMMER,
                    message: `failed to auto-enable group config option '${optionFmt}'`,
                    detail: `the specified group configuration key '${group}' does not exist`
                });
            }
        }
        else if (!isWpwBuildOptionsRootKey(option, false, build.options))
        {   throw build.addMessage({
                code: SpmhMessageUtils.Code.ERROR_SHITTY_PROGRAMMER,
                message: `failed to auto-enable root config option '${option}'`,
                detail: `the specified options key '${optionFmt}' does not exist`
            });
        }

        const grpCfg = group ? build.options[group] : null;
        if (group && !grpCfg) {
            build.options[group] = /** @type {any} */({});
        }
        let optCfg = !group ? build.options[/** @type {any} */(option)] : grpCfg[/** @type {any} */(option)];
        if (!group && !optCfg) {
            optCfg = build.options[/** @type {any} */(option)] = {};
        }

        if (!force && (optCfg === false || (isObject(optCfg) && optCfg.enabled === false))) // &&
            // !properties.find((p) => isNulled((optCfg || {})[p]) && !p.includes("."))))
        { return null; }

        if (group)
        {
            if (grpCfg.enabled !== true)
            {   l.write(`      auto-enable group '${group}|${option}' [${(typeof grpCfg).constructor.name}]`, 3, this._lPad);
                grpCfg.enabled = true; results.push(`${group} [group]`);
                mergeIfStrong(grpCfg, build.wrapper.schema.defaults((typeof grpCfg).constructor.name), { enabled: true });
            }
            grpCfg[/** @type {any} */(option)] ||= {};
            optCfg = grpCfg[/** @type {any} */(option)];
        }

        if (optCfg.enabled !== true) // 'option'
        {   l.write(`      auto-enable root option '${option}' [${(typeof optCfg).constructor.name}]`, 3, this._lPad);
            if (!isObject(grpCfg)) { apply(grpCfg, { enabled: false }); }
            optCfg.enabled = true; results.push(optionFmt);
            mergeIfStrong(optCfg, build.wrapper.schema.defaults((typeof optCfg).constructor.name), { enabled: true });
        }

        properties.filter((p) => !!p && optCfg[p] !== false || force === true).forEach((p) =>
        {
            l.write(`      auto-enable adjacent option '${p}'`, 1, this._lPad);
            if (!p.includes(".")) {
                apply(optCfg, { [p]: true });
            }
            else {
                merge(optCfg, fromDotPath(p, true));
            }
            results.push(`${optionFmt}.${p}`);
        });

        l.value("      resulting option configuration", JSON.stringify(optCfg), 4, this._lPad);
        return optCfg;
    }


    /**
     * @since 1.10.0
     * @param {WpwBuild} build
     * @param {WebpackLibrary | undefined} [def]
     * @returns {WebpackLibrary | undefined}
     */
    getLibraryType(build, def)
    {
        const config = build.config;
        let /** @type {WebpackLibrary | undefined} */lib;

        if (!build.isAppOrLib && !build.isWeb) {
            return lib;
        }
        else if(isWebpackLibrary(config.library))
        {
            lib = config.library;
        }
        else if (isWebpackLibrary(config.options.output?.libraryTarget))
        {
            lib = config.options.output.libraryTarget;
        }
        else if (isWebpackLibrary(config.options.output?.library))
        {
            lib = config.options.output.library;
        }
        else if (config.source?.tsconfig?.compilerOptions?.module)
        {
            const tsModule = build.tsconfig.compilerOptions.module.toLowerCase();
            if (isWebpackLibrary(tsModule)) {
                lib = tsModule;
            }
            // else if (/es(?:20[02]{2}|next)/i.test(tsModule)) {
            //     lib = "modern-module";
            // }
            else if (/es(?:[5-8]|[0-9]{4}|next)/i.test(tsModule)) {
                lib = "module";
            }
        }

        if (!lib && isWebpackLibrary(build.pkgJson.type))
        {
            lib = build.pkgJson.type;
        }

        return lib || def;
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @throws {Error}
     */
    validateOptions(build)
    {
        if (build.options.npmiso?.enabled && (build.options.vsceiso?.enabled ||
            !!build.buildConfigs.find((b) => b.options.vsceiso?.enabled && b.options.vsceiso.dist === build.options.npmiso.dist)))
        {
            throw new Error("npm/vsce iso release options cannot be set to the same output/dist directory");
        }
    }
}


module.exports = WpwBuildOptionsFinalizer;