core_build.js

/**
 * @file src/core/build.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 * @description main class representing a `build` that produces output assets
 *                 .-.                       .-.                           .;;;;.          .-.     .-.
 * .;.       .-.   (_) )-.   .;.       .-.   (_) )-.       .;.   .-.       ' .;'  `       ;' (_)   (_) )-.
 *    `;     ;'       .:   \    `;     ;'       .: __)       ;;   ;          .;'         .:'          .:   \
 *     ;;    ;       .:'    )    ;;    ;       .:'   `.     ;;    :         .;'         .:'          .:'    \
 *    ;;  ;  ;;    .-:. `--'    ;;  ;  ;;      :'      )   ;;     ;        .;'        .-:.    .-.  .-:.      )
 *    `;.' `.;'   (_/           `;.' `.;'   (_/  `----'    `;.__.:     .;;;;;;;;;'   (_/ `;._.    (_/  `----'
 */

const WpwBase = require("./base");
const WpwLogger = require("../log/log");
// const WpwTscService = require("../services/tsc");
const WpwSourceCodeService = require("./source");
const { withColor } = require("@spmhome/log-utils");
const WpwBuildOptionsFinalizer = require("./options");
const WpwSnapshotService = require("../services/snapshot");
const { printBuildProperties, printBuildMessages } = require("../utils/print");
const { SpmhWarning, SpmhInfo, SpmhError, SpmhMessageUtils } = require("@spmhome/log-utils");
const { isWpwBuildType, isWebpackTarget, WpwBuildConfigKeys } = require("../types/constants");
const { getUniqBuildKey, isWpwBuildOptionsKey, wpwNodeModulesPath, wpwPath } = require("../utils/utils");
const {
    createDirSync, existsSync, relativePath, resolvePath, nodejsModulesGlobalPath, relativePathEx
} = require("@spmhome/cmn-utils");


/**
 * @augments WpwBase
 * @implements {IWpwBuildConfig}
 */
class WpwBuild extends WpwBase
{
    /**
     * @private
     * @type {SpmhError[]}
     */
    static _errorsGlobal = [];
    /**
     * @private
     * @type {IWpwRuntimeExternal[]}
     */
    static _nodeModules = [];
    /**
     * @private
     * @type {WebpackCompilation}
     */
    _compilation;
    /**
     * @private
     * @type {WebpackCompilation}
     */
    _compilationThis;
    /**
     * @private
     * @type {WebpackCompiler}
     */
    _compiler;
    /**
     * @private
     * @type {IWpwBuildConfig}
     */
    _config;
    /**
     * @private
     * @type {boolean}
     */
    _autoConfig;
    /**
     * @type {WpwCrypto}
     */
    crypto;
    /**
     * @type {boolean}
     */
    debug;
    /**
     * @type {WpwWebpackEntry}
     */
    entry;
    /**
     * @private
     * @type {boolean}
     */
    _disposed;
    /**
     * @private
     * @type {SpmhError[]}
     */
    _errors;
    /**
     * @private
     * @type {RegExp}
     */
	_externalsRgx;
    /**
     * @private
     * @type {SpmhMessage[]}
     */
    _info;
    /**
     * @type {WebpackLibrary | undefined}
     */
    library;
    /**
     * @type {WpwWebpackLoaderType | undefined}
     */
    loader;
    /**
     * @type {WpwLogOptions}
     */
    log;
    /**
     * @type {WpwLogger}
     */
    logger;
    /**
     * @type {boolean | undefined}
     */
    main;
    /**
     * @type {WpwWebpackMode}
     */
    mode;
    /**
     * @private
     * @type {IWpwRuntimeExternal[]}
     */
    _nodeModules;
    /**
     * @private
     * @type {WpwNodeModulesPaths}
     */
    _nodeModulesPaths;
    /**
     * @type {WpwBuildOptions}
     */
    options = {};
    /**
     * @type {WpwRcPaths}
     */
    paths;
    /**
     * @private
     * @type {WpwRcVirtualPaths}
     */
    _vpaths;
    /**
     * @private
     * @type {boolean}
     */
    _rebuild;
    /**
     * @type {WpwStaticResources}
     */
    resources;
    /**
     * @private
     * @type {WpwSourceCode}
     */
    _scm;
	/**
	 * @type {WpwSnapshotService}
	 */
	_ssCache;
    /**
     * @private
     * @type {"idle" | "initializing" | "waiting" | "running" | "done" | "failed" | "disposed"}
     */
    _state;
    /**
     * @type {WebpackTarget | WebpackTarget[]}
     */
    target;
    /**
     * @type {WpwBuildType}
     */
    type;
    /**
     * @type {WpwVirtualEntry}
     */
    virtualEntry;
    /**
     * @private
     * @type {number}
     */
    _elapsed = 0;
    /**
     * @private
     * @type {{ msg: string, ct: number, tId: number | NodeJS.Timeout, fn: "info" | "error" | "warn"}}
     */
    _lastMsgTag = { msg: "", ct: 0, tId: 0, fn: "error" };
    /**
     * @private
     * @type {SpmhWarning[]}
     */
    _warnings;
    /**
     * @private
     * @type {WebpackType}
     */
    _wp;
    /**
     * @private
     * @type {WpwWebpackConfig}
     */
    _wpc;
    /**
     * @readonly
     * @private
     * @type {WpwWrapper}
     */
    _wrapper;


    /**
     * @param {WpwWrapper} wpw
     * @param {WebpackType} wp
     * @param {IWpwBuildConfig} config
     */
    constructor(wpw, wp, config)
    {
        super(config);
        this._wp = wp;
        this.name = "";
        this._info = [];
        this._errors = [];
        this._warnings = [];
        this._nodeModules = [];
        this._wrapper = wpw;
        this._rebuild = false;
        this._disposed = false;
        this._state = "initializing";
    }


    get autoConfig() { return this._autoConfig !== false && this.wrapper.autoConfig !== false; }
    /** @private */set autoConfig(v) { this._autoConfig = v; }
    get autoConfigTs() { return this._scm.autoConfig === true; }
    get builds() { return this._wrapper.builds; }
    get buildConfigs() { return this._wrapper.buildConfigs; }
    get buildCount() { return this._wrapper.buildCount; }
    get cacheDir() { return this.virtualEntry.dirStore; }
    get cli() { return this._wrapper.cli; }
    get compilation() { return this._compilation; }
    set compilation(c) { this._compilation = c; this.ssCache?.updateRuntimeInstances(this._compiler, c); }
    get compilationThis() { return this._compilationThis; }
    set compilationThis(c) { this._compilationThis = c; }
    get compiler() { return this._compiler; }
    set compiler(c) { this._compiler = c; }
    get config() { return this._config; }
    get elapsed() { return this._elapsed; }
    set elapsed(v) { this._elapsed = v; }
    get errorCount() { return this._errors.length; }
    get errors() { return this._errors; }
    get errorsGlobal() { return  WpwBuild._errorsGlobal; }
	get externalsRgx() { return this._externalsRgx; }
	set externalsRgx(r) { this._externalsRgx = r; }
    get info() { return this._info; }
    get infoCount() { return this._info.length; }
    get hasError() { return this._errors.length > 0; }
    get hasErrorOrWarning() { return this.hasError || this.hasWarning; }
    get hasErrorOrWarningOrInfo() { return this.hasError || this.hasInfo || this.hasWarning; }
    get hasInfo() { return this._info.length > 0; }
    get hasGlobalError() { return WpwBuild._errorsGlobal.length > 0; }
    get hasWarning() { return this._warnings.length > 0; }
    get isAnyApp() { return this.isApp || this.isWebApp; }
    get isAnyLib() { return this.isLib || this.isWebLib; }
    get isAnyAppOrLib() { return this.isAppOrLib || this.isWebAppOrLib; }
    get isApp() { return this.type === "app"; }
    get isAppOrLib() { return this.isApp || this.isLib || this.isWebApp; }
    get isDevMode() { return this.mode === "development"; }
    get isDocs() { return [ "docs", "doxygen", "extjsdoc", "jsdoc" ].includes(this.type); }
    get isExpo() { return this.source.info.language.startsWith("expo."); }
    get isJs() { return this._scm.isJs; }
    get isJsTs() { return this._scm.isJsTs; }
    get isLib() { return this.type === "lib"; }
    get isLogDisabled() { return this.logger.isDisabled; }
    get isLogEnabled() { return this.logger.isEnabled; }
    get isMain() {
        return !!this.main || (this.isAnyAppOrLib &&
            ((this.buildConfigs.find((c) => !c.disabled && (c.type === "app")) &&
             !this.options.output?.name || this.options.output.name === this.name) ||
            (this.buildConfigs.find((c) => !c.disabled && (c.type === "lib")) &&
             !this.options.output?.name || this.options.output.name === this.name)));
    }
    get isModule() { return this.library === "module" || this.library === "modern-module"; }
    get isMultiTarget() {
        return (this.options.npmiso && this.options.npmiso.dist !== this.getDistPath()) ||
            (this.options.vsceiso && this.options.vsceiso.dist !== this.getDistPath()) ||
            this.isAnyAppOrLib && this.buildConfigs.find((b) =>
                !b.disabled && b.name !== this.name && b.target !== this.target && b.type === this.type);
    }
    get isNodeApp() { return !this.isWeb && this.isApp; }
    get isOnlyBuild() { return this._wrapper.isSingleBuild; }
    get isProdMode() { return this.mode === "production"; }
    get isRebuild() { return this._rebuild; }
    set isRebuild(v) { this._rebuild =  v; }
    get isReact() { return this.source.info.language.startsWith("react."); }
    get isReactNative() { return this.source.info.language === "reactnative"; }
    get isResource() { return this.type === "resource"; }
    get isSchema() { return this.type === "schema"; }
    get isScript() { return this.type === "script"; }
    get isWeb() { return this.target.includes("web") || this.target.includes("webworker"); }
    get isTests() { return this.type === "tests"; }
    get isTs() { return this._scm.isTs; }
    get isTsJs() { return this._scm.isTsJs; }
    get isTypes() { return this.type === "types"; }
    get isTranspiled() { return this.isAppOrLib || this.isTypes || this.isWebAppOrLib; }
    get isWebApp() { return this.isWeb && this.type === "webapp"; }
    get isWebAppOrLib() { return this.isWebApp || this.isWebLib; }
    get isWebLib() { return this.type === "lib" && this.isWeb; }
    get isWebWorker() { return this.target.includes("webworker"); }
    get lastError() { return this._errors[this._errors.length - 1]; }
    get nameAlias() { return this.options.output?.name || this.name; }
	get nodeModules() { return this._nodeModules; }
	get nodeModulesGlobal() { return WpwBuild._nodeModules; }
    get nodeModulesPath() { return this._nodeModulesPaths.base; }
    get nodeModulesPathCtx() { return this._nodeModulesPaths.ctx; }
    get nodeModulesPathGbl() { return this._nodeModulesPaths.global; }
    get nodeModulesPathWpw() { return this._nodeModulesPaths.wpw; }
    get nodeModulesPaths() { return this._nodeModulesPaths; }
    get pkgJson() { return this._wrapper.pkgJson; }
    get pkgJsonFilePath() { return this._wrapper.pkgJsonFilePath; }
    get project() { return this.wrapper.settings.project; }
    get schema() { return this.wrapper.schema; }
    get source() { return this._scm; }
    get sourceInfo() { return this._scm.info; }
    get ssCache() { return this._ssCache; }
    get state() { return this._state; }
    set state(s) { this._state = s; }
    get stats() { return this._wrapper.stats; }
    get statsfile() { return this._wrapper.statsfile; }
    get tsc() { return this._scm.tsc; }
    get tsCompilerOptions() { return this.tsc?.compilerOptions || this._obj_.create(); }
    get tsconfig() { return this._scm.tsc.tsconfig; }
    get vpaths() { return this._vpaths; }
    /** @private */set vpaths(v) { this._obj_.apply(this._vpaths, v); }
    get warnings() { return this._warnings; }
    get warningCount() { return this._warnings.length; }
    get wp() { return this._wp; }
    get wpc() { return this._wpc; }
    set wpc(v) { this._wpc =  v; }
    get wpw() { return this._wrapper; }
    get wpwPath() { return wpwPath(); }
    get wrapper() { return this._wrapper; }


    /**
     * @param {SpmhMessageInfo | Error} info
     * @param {boolean} [ifFirst]
     * @returns {SpmhError}
     */
    addMessage(info, ifFirst)
    {
        let e;
        const isJsErr = this._types_.isError(info) && !SpmhMessageUtils.isSpmh(info);
        if (SpmhMessageUtils.isMessageInfo(info))
        {
            this._obj_.applyIf(info, { build: this, capture: this.addMessage });
            if (SpmhMessageUtils.isInfoCode(info.code))
            {
                e = this.pushMessageQ(this._info, info, ifFirst, "info", "spmh_blue");
            }
            else if (SpmhMessageUtils.isWarningCode(info.code))
            {
                e = this.pushMessageQ(this._warnings, info, ifFirst, "warn", "lightyellow");
            }
            else {
                e = this.pushMessageQ(this._errors, info, ifFirst, "error", "lightpink");
            }
        }
        else if (isJsErr)
        {
            info = {
                build: this,
                message: info.message,
                capture: this.addMessage,
                exception: this._obj_.apply({}, info),
                code: SpmhMessageUtils.Code.ERROR_GENERAL
            };
            e = this.pushMessageQ(this._errors, info, ifFirst, "error", "spmh_blue");
        }
        else if (SpmhMessageUtils.isInfo(info))
        {
            e = this.pushMessageQ(this._info, info, ifFirst, "info", "lightblue");
        }
        else if (SpmhMessageUtils.isWarning(info))
        {
            e = this.pushMessageQ(this._warnings, info, ifFirst, "warn", "warning");
        }
        else if (SpmhMessageUtils.isError(info))
        {
            while (SpmhMessageUtils.isError(info.exception)) {
                info.exception = info.exception.exception;
            }
            e = this.pushMessageQ(this._errors, info, ifFirst, "error", "error");
        }
        return e;
    }


    /**
     * @param {SpmhSuggestionInfo} info
     * @param {boolean} [ifFirst]
     * @returns {SpmhMessage}
     */
    addSuggestion(info, ifFirst)
    {
        return this.addMessage(this._obj_.apply({ message: "general suggestion" }, info), ifFirst);
    }


    createVirtualBuildPaths()
    {
        if (!existsSync(this.virtualEntry.dir)) {
            createDirSync(this.virtualEntry.dir);
        }
        if (!existsSync(this.virtualEntry.dirDist)) {
            createDirSync(this.virtualEntry.dirDist);
        }
        if (!existsSync(this.virtualEntry.dirBuild)) {
            createDirSync(this.virtualEntry.dirBuild);
        }
        if (!existsSync(this.virtualEntry.dirStore)) {
            createDirSync(this.virtualEntry.dirStore);
        }
    }


    /**
     * @override
     */
    dispose() { super.dispose(); this._state = "disposed"; }


    /**
     * @template {WpwGetPathOptions | undefined} P
     * @param {P} [options]
     * @returns {WpwGetBuildPathResult<P>}
     */
    getBasePath = (options) => (!options || !options.ctx ? this.getRcPath("base", options) : this.getRcPath("ctx", options));


    /**
     * @param {string} nameOrType
     * @param {boolean} [isName]
     * @returns {WpwBuild | undefined}
     */
    getBuild = (nameOrType, isName) => this.wrapper.getBuild(nameOrType, isName);


    /**
     * @param {string} nameOrType
     * @param {boolean} [isName]
     * @returns {IWpwBuildConfig | undefined}
     */
    getBuildConfig = (nameOrType, isName) => this.wrapper.getBuildConfig(nameOrType, isName);


    /**
     * @param {function(IWpwBuildConfig): boolean} cb
     * @param {any} thisArg
     */
    getBuildConfigBy = (cb, thisArg) => this.wrapper.getBuildConfigBy(cb, thisArg);


    getBuildConfigMain = (web) => this.getBuildConfig(!web ? "app" : "webapp") ||
                                  this.getBuildConfig("lib") || this.getBuildConfig("jsdoc");


    /**
     * @param {WebpackSource} source
     * @returns {string} string
     */
    getContentHash(source)
    {
        return this._ssCache.getContentHash(source.buffer());
    }


    /**
     * @template {WpwGetPathOptions | undefined} P
     * @param {P} [options]
     * @returns {WpwGetBuildPathResult<P>}
     */
    getContextPath = (options) => this.getRcPath("ctx", options);


    /**
     * @template {WpwGetPathOptions | undefined} P
     * @param {P} [options]
     * @returns {WpwGetBuildPathResult<P>}
     */
    getDistPath = (options) =>this.getRcPath("dist", options);


    /**
     * @param {string} path path to file
     * @param {string} [basePath] path base relative from
     * @returns {string} relative path to file, root @ context directtory
     */
    getEmitRelPath(path, basePath)
    {
        let filePathRel = path;
        if (!this._path_.isAbsPath(path))
        {   if (!basePath)
            {   path = resolvePath(this.virtualEntry.dirDist, path);
                if (!existsSync(path)) {
                    path = resolvePath(this.virtualEntry.dirBuild, path);
                    if (!existsSync(path)) {
                        path = resolvePath(this.getSrcPath(), path);
                        if (!existsSync(path)) {
                            path = resolvePath(this.getContextPath(), path);
                            if (!existsSync(path)) {
                                path = resolvePath(this.getBasePath(), path);
            }   }   }   }   }
            else { path = resolvePath(basePath, path); }
        }
        if (basePath) {
            filePathRel = relativePathEx(basePath, path, { psx: true });
        }
        else if (path.startsWith(this.virtualEntry.dirDist)) {
            filePathRel = relativePathEx(this.virtualEntry.dirDist, path, { psx: true });
        }
        else if (path.startsWith(this.virtualEntry.dirBuild)) {
            filePathRel = relativePathEx(this.virtualEntry.dirBuild, path, { psx: true });
        }
        else if (path.startsWith(this.getSrcPath())) {
            filePathRel = relativePathEx(this.getSrcPath(), path, { psx: true });
        }
        else if (path.startsWith(this.getContextPath())) {
            filePathRel = relativePathEx(this.getContextPath(), path, { psx: true });
        }
        else { filePathRel = relativePathEx(this.getBasePath(), path, { psx: true }); }
        return filePathRel;
    }


    /**
     * @private
     * @template {WpwGetPathOptions | undefined} P
     * @param {WpwRcPathsKey} pathKey
     * @param {P} [options]
     * @returns {WpwGetBuildPathResult<P>}
     */
    getRcPath(pathKey, options)
    {
        const opts = options || /** @type {WpwGetPathOptions} */({}),
            basePath = opts.ctx ? this.paths.ctx : this.paths.base,
            build = opts.build ? this.getBuildConfig(opts.build) : undefined;

        const _getPath = /** @param {string | undefined} path */(path) =>
        {
            const oPath = path;
            if (path)
            {   if (opts.rel || opts.relBase)
                {   if (this._path_.isAbsPath(path))
                    {   let absPath = path;
                        if (opts.path)  {
                            absPath = this._path_.isAbsPath(opts.path) ? opts.path : this._path_.resolvePath(path, opts.path);
                        }
                        if (opts.stat && !this._fs_.existsSync(absPath)) {
                            path = undefined;
                        }
                        else if (absPath === basePath) {
                            path = ".";
                        }
                        else
                        {   path = this._path_.relativePath(!opts.relBase ? basePath : oPath, absPath);
                            if (opts.dot) {
                                path = `.${this._path_.pathSep}${path}`;
                            }
                        }
                    }
                    else
                    {   let absPath = this._path_.resolvePath(basePath, path);
                        if (opts.path)
                        {   if (this._path_.isAbsPath(opts.path)) {
                                opts.path = this._path_.relativePath(absPath, opts.path);
                            }
                            absPath = this._path_.resolvePath(absPath, opts.path);
                        }
                        if (opts.stat && !this._fs_.existsSync(absPath)) {
                            path = undefined;
                        }
                        else if (absPath === basePath) {
                            path = ".";
                        }
                        else
                        {   path = this._path_.relativePath(!opts.relBase ? basePath : oPath, absPath);
                            if (opts.dot) {
                                path = `.${this._path_.pathSep}${path}`;
                            }
                }   }   }
                else
                {   if (!this._path_.isAbsPath(path)) {
                        path = this._path_.resolvePath(basePath, path);
                    }
                    if (opts.path) {
                        if (this._path_.isAbsPath(opts.path)) {
                            opts.path = this._path_.relativePath(path, opts.path);
                        }
                        path = this._path_.resolvePath(path, opts.path);
                    }
                    if (opts.stat && !this._fs_.existsSync(path)) {
                        path = undefined;
                    }
                }
            }
            else if (opts.fallback)
            {
                path = _getPath(this.getBuildConfig("app")?.paths[pathKey]) || _getPath(basePath);
            }

            return path ? !opts.psx ? path.replace(/[\\/]/g, this._path_.pathSep) : this._path_.fwdSlash(path) : path;
        };

        return !build ? _getPath(this.paths[pathKey]) : _getPath(build.paths[pathKey]);
    }


    /**
     * @private
     * @returns {IWpwRcPaths}
     */
    getRootPathsConfig = () => this.wrapper.getRootPathsConfig(this.isWebApp);


    /**
     * @returns {string}
     */
    getRootBasePath = () => this.getRootPathsConfig().base;


    /**
     * @returns {string}
     */
    getRootCtxPath = () => this.getRootPathsConfig().ctx;


    /**
     * @returns {string}
     */
    getRootDistPath = () => this.getRootPathsConfig().dist;


    /**
     * @returns {string}
     */
    getRootSrcPath = () => this.getRootPathsConfig().src;


    /**
     * @template {WpwGetPathOptions | undefined} P
     * @param {P} [options]
     * @returns {WpwGetBuildPathResult<P>}
     */
    getSrcPath = (options) => this.getRcPath("src", options);


    /**
     * @returns {RegExp}
     */
    getSrcPathRegExp = () =>
    {
        const srcPath = this.getSrcPath(),
              escapeRegExp = (/** @type {string} */txt) => txt.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") || "";
        // eslint-disable-next-line stylistic/max-len
        if (/^(?:\.{1,2}|(?:\.[\\/])?[a-zA-Z0-9_][~@!:\\*a-zA-Z0-9_.-\\/:]+ *[,|;]) *(?:\.{1,2}|(?:\.[\\/])?[a-zA-Z0-9_][~@!:*a-zA-Z0-9_.-\\/:]+ *[|,;]?)$/.test(srcPath)) {
            return new RegExp(srcPath.split(/;|,|\|/).map((p) => escapeRegExp(p.trim())).join("|")); // delimited by , or ; or |
        } return new RegExp(escapeRegExp(srcPath)); // path string or regex string
    };


    /**
     * @private
     * @param {IWpwBuildConfig} config
     * @returns {WebpackTarget | WebpackTarget[]}
     */
    getTarget(config)
    {
        /** @type {WebpackTarget[]} */
        const target = !config.target ? [] : this._arr_.popBy(this._arr_.asArray(config.target), (t) => !isWebpackTarget(t));
        if (target.length === 0)
        {   if (config.type === "webapp" || (/^(?:web|browser)/).test(config.name)) {
                target.push("web");
            }
            else if ((/^(?:web(?:worker|view))/).test(config.name)) {
                target.push("webworker");
            }
            else { target.push("node"); }
        }
        return target.length === 1 ? target[0] : target;
    }


    /**
     * @template {WpwGetPathOptions | undefined} P
     * @param {P} [options]
     * @returns {WpwGetBuildPathResult<P>}
     */
    getTempPath = (options) => this.getRcPath("temp", options);


    /**
     * @private
     * @param {IWpwBuildConfig} config
     * @returns {WpwBuildType}
     */
    getType(config)
    {
        /** @type {WpwBuildType | undefined} */
        let type;
        if (isWpwBuildType(config.type)) { type = config.type; return type; }
        if (isWpwBuildType(config.name)) { type = config.name; }
        else if ((/\btyp(?:es|ings)/).test(config.name)) { type = "types"; }
        else if ((/\btests?(?:-?suite)?/).test(config.name)) { type = "tests"; }
        else if ((/\blib.+$|^\w+-utils(?:-.+?)?$/).test(config.name)) { type = "lib"; }
        else if ((/\b(?:web(?:worker|app|views|)|www[.-_].+)$/).test(config.name)) { type = "webapp"; }
        else if ((/\b(?:js)?docs?|documentation|doxy?(?:gen)?|help|man|support/).test(config.name)) { type = "doc"; }
        if (!type)
        {
            type = "app";
            this.addSuggestion({
                code: SpmhMessageUtils.InfoCode.INFO_CONFIG_SUGGESTION,
                message: "the build type was not specified in the rc configuration and has been auto-set to 'app'",
                suggest: "ensure that each merged build configuration is set to the desired " +
                        "type to avoid possibly un-noticed issues in the build output"
            });
        }
        return type;
    }


    /**
     * @returns {Promise<WpwBuild>}
     * @param {string} lPad
     */
    async init(lPad)
    {
        try
        {   const lPadIn = lPad + "      ";
            //
            // finalize instance configuration
            //
            this.initConfig();
            this._rebuild = !existsSync(this.paths.dist);
            this._wpc = /** @type {WpwWebpackConfig} */({});
            //
            // initialize logger
            //
            this.disposables.push(
                this.logger = /** @type {WpwLogger} */(WpwLogger.getLoggerInst(this.log))
            );
            this.logger.write(
                `configure build wrapper for '${this.name}::${this._arr_.asArray(this.target).join("|")}`, 1, lPad
            );
            //
            // prepare cache and compilation build directories
            //
            this.logger.write("   resolve virtual build output paths", 1, lPad);
            await this.resolveVirtualPaths();
            //
            // initialize build options utility, set initial target lib (subject to change)
            //
            const boFinalizer = new WpwBuildOptionsFinalizer(this, lPadIn);
            this.library = boFinalizer.getLibraryType(this);
            //
            // initialize ts sourcecode service
            //
            // this.disposables.push(this._tsc = new WpwTscService({ build: this }));
            this.disposables.push(this._scm = new WpwSourceCodeService(this._config.source, this));
            await this._scm.init(lPadIn);
            this._config.source = this._obj_.merge({}, this._scm.config);
            //
            // finalize build options
            //
            this.logger.write("   finalize build configuration", 1, lPad);
            boFinalizer.configure();
            const bCfg = this._obj_.pickBy(this.options, (k) => isWpwBuildOptionsKey(null, k));
            this.logger.value(
                "   finalized build", this._json_.safeStringify(bCfg, null, this.logger.level >= 3 ? 3 : undefined), 2, lPad
            );
            //
            // validate rc configuration using buildconfig json schema
            //
            this.logger.write("   validate build configurations schema", 1, lPad);
            this.schema.validate(this, "WpwBuildConfig", this.logger, lPadIn, WpwBuildConfigKeys);
            //
            // initialize compilation filesystem snapshot cache
            //
            this.logger.write("   initialize snapshot cache service", 1, lPad);
            this.disposables.push(this._ssCache = new WpwSnapshotService({ build: this }));

            this.logger.success(`successfully configured execution wrapper for '${this.name}' build`, 1, lPad);
            return this;
        }
        catch(e)
        {   if (!this.logger) {
                console.error(e);
            }
            else
            {   this.logger.error("dump invalid configuration:");
                printBuildProperties(this, withColor(this.logger.icons.error, this.logger.color), "");
                this.logger.blank(1, withColor(this.logger.icons.error, this.logger.color));
            }
            throw e;
        }
    }


    /**
     * @private
     */
    initConfig()
    {
        const targets = this._arr_.asArray(this.target),
              config = this._obj_.merge({}, this.initialConfig);
        config.target ||= this.getTarget(config);
        config.type = this.getType(config);
        // this._obj_.mergeIf(config, this.wrapper.targetConfigs[config.target] || {})
        config.target = this._arr_.asArray(config.target);
        config.vpaths ||= { base: config.paths.dist || "" };
        this.validateConfig(config);
        this._obj_.apply(this, this._obj_.pickNot(config, "source"));
        this._obj_.apply(this.log, { envTag1: this.name, envTag2: targets.join("|") });
        this._obj_.apply(config.log, { envTag1: this.name, envTag2: targets.join("|") });
        this._obj_.apply(this, { _config: config });
    }


    /**
     * @param {IWpwRuntimeExternal} info
     * @returns {IWpwRuntimeExternal[]}
     */
    pushImportedModule(info)
    {
        let alreadyAdded = !!this._nodeModules.find((e) => e.name === info.name);
        if (!alreadyAdded)
        {   this._nodeModules.push(info);
            alreadyAdded = !!WpwBuild._nodeModules.find((e) => e.name === info.name);
            if (!alreadyAdded) {
                WpwBuild._nodeModules.push(this._obj_.apply({}, info));
            }
        }
        return this._nodeModules;
    }


    /**
     * @private
     */
    resolveVirtualPaths()
    {
        return /** @type {Promise<void>} */(new Promise((ok, _fail) =>
        {
            const dirCtx = this.getContextPath(),
                  filenameTl = `v_${this.nameAlias}`,
                  uniqKey = getUniqBuildKey(this, true),
                  uniqKey2 = getUniqBuildKey(this, false),
                  vBaseDir = this._path_.resolvePath(this.wrapper.cacheDir, this.nameAlias, uniqKey2),
                  vDistDir = this._path_.resolvePath(vBaseDir, "dist"),
                  vBuildDir = this._path_.resolvePath(vBaseDir, "build");
            this.virtualEntry = {
                uniqKey,
                dir: vBaseDir,
                file: filenameTl,
                chunk: filenameTl,
                dirDist: vDistDir,
                dirBuild: vBuildDir,
                dirWpCache: vBaseDir,
                dirDistRelToProj: relativePath(dirCtx, vDistDir),
                dirBuildRelToProj: relativePath(dirCtx, vBuildDir),
                dirBuildRelToDist: relativePath(vDistDir, vBuildDir),
                dirDistRelToBuild: relativePath(vBuildDir, vDistDir),
                dirStore: this._path_.resolvePath(vBaseDir, "store"),
                filePathAbs: this._path_.resolvePath(vBaseDir, filenameTl),
                filePathRel: relativePath(dirCtx, this._path_.joinPath(vBaseDir, filenameTl))
            };
            this.createVirtualBuildPaths();
            this._nodeModulesPaths = {
                all: [],
                wpw: wpwNodeModulesPath(),
                global: nodejsModulesGlobalPath(),
                base: this._path_.joinPath(this.getBasePath(), "node_modules"),
                ctx: this._path_.joinPath(this.getContextPath(), "node_modules")
            };
            if (!this._fs_.existsSync(this._nodeModulesPaths.ctx)) {
                this._nodeModulesPaths.ctx = this._nodeModulesPaths.base;
            }
            this._arr_.pushUniq2(
                this._nodeModulesPaths.all, this.global.isWin32, this._nodeModulesPaths.ctx,
                this._nodeModulesPaths.base, this._nodeModulesPaths.wpw, this._nodeModulesPaths.global
            );
            ok();
        }));
    }


    /**
     * @param {boolean} [failed]
     */
    printMessages(failed) { printBuildMessages(this, !!failed, this.log.suppress); }


    /**
     * @param {string | null} lPad
     * @param {WpwLogColor | SpmhLogColorValue} [seeDetailsClr]
     * @param {"error" | "info" | "warn" } [newFn]
     * @param {string} [newMsg]
     */
    printMessageQ(lPad, seeDetailsClr, newFn, newMsg)
    {
        if (this._lastMsgTag.tId) {
            clearTimeout(this._lastMsgTag.tId);
        }
        if (lPad !== null && this._lastMsgTag.ct > 0)
        {   const l = this.logger,
                  lFn = this._lastMsgTag.fn ? l[this._lastMsgTag.fn] : l[newFn],
                  tag = l.tag(`repeated ${this._lastMsgTag.ct} times`, seeDetailsClr, "default");
            // lFn(lPad + this._lastMsgTag.msg.replace(/ {3,}/g, "").replace(":", `${tag} :`));
            lFn(lPad + this._lastMsgTag.msg.replace(":", `${tag} :`));
        }
        this._lastMsgTag.ct = 0;
        this._lastMsgTag.tId = 0;
        this._lastMsgTag.fn = newFn;
        this._lastMsgTag.msg = newMsg;
    }


    /**
     * @private
     * @param {SpmhMessage[]} msgQ
     * @param {SpmhMessageInfo | SpmhSuggestionInfo | SpmhMessage} info
     * @param {boolean} ifFirst
     * @param {"error" | "info" | "warn"} fn
     * @param {WpwLogColor | SpmhLogColorValue} seeDetailsTagClr
     * @returns {SpmhMessage}
     */
    pushMessageQ = (msgQ, info, ifFirst, fn, seeDetailsTagClr) =>
    {
        const qIsEmpty = this._types_.isEmpty(msgQ),
              isInfo = SpmhMessageUtils.isMessageInfo(info),
              msg = !isInfo ? info : SpmhMessageUtils.isErrorCode(info.code) ?
                    new SpmhError(info) : (SpmhMessageUtils.isWarningCode(info.code) ?
                                         new SpmhWarning(info) : new SpmhInfo(info));

        if (qIsEmpty || (!ifFirst && msgQ.every((m) => !msg.isEqual(m))))
        {   const l = this.logger, lPad = isInfo ? info.lPad || l.lastPad : l.lastPad,
                  msgFmt = msg.xMessage.toString().split("\n")[0].replace(/ {2,}/g, "").trim() +
                          ` ${ l.tag(` ${msg.loc.link} `, seeDetailsTagClr, "default")}`,
                  logIt = !isInfo || (info.lSkip !== true && this.logger.isLevelLogged(info.lLvl));
            if (msgFmt !== this._lastMsgTag.msg)
            {   if (this._lastMsgTag.tId) {
                    this.printMessageQ(logIt ? lPad : null, seeDetailsTagClr, fn, msgFmt);
                }
                if (logIt) {
                    l[fn](msgFmt, lPad);
                }
            }
            else if (logIt)
            {   ++this._lastMsgTag.ct;
                this._lastMsgTag.tId = setTimeout(this.printMessageQ, 125, lPad);
            }
            if (fn === "error") {
                WpwBuild._errorsGlobal.push(msg);
            }
            msgQ.push(msg);
        }
        return msg;
    };


    /**
     * @private
     * @param {IWpwBuildConfig} config
     */
    validateConfig(config)
    {
        const _err = (/** @type {string} */ p) => new SpmhError(
        {   code: SpmhMessageUtils.Code.ERROR_RESOURCE_MISSING,
            message: `config validation failed for build ${this.name}: property ${p}`
        });
        if (!config.name) { throw _err("config.name"); }
        if (!config.type) { throw _err("config.type"); }
        if (!config.mode) { throw _err("config.mode"); }
        if (!config.target) { throw _err("config.target"); }
    }
}


module.exports = WpwBuild;