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