services_tsc.js

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

const WpwService = require("./base");
const { isWpwTsCompilerOptionLib } = require("../utils");
const { merge, pushUniq, applyIf } = require("@spmhome/type-utils");
const {
    basename, existsSync, deleteFile, isAbsPath, resolvePath, relativePath, isTsJsConfigFile
} = require("@spmhome/cmn-utils");


/**
 * @class WpwTscService
 */
class WpwTscService extends WpwService
{
    /**
     * @privates
     * @type {number}
     */
    static _tmpFileId = 0;
    /**
     * @privates
     * @type {string}
     */
    static _cacheKeyTsConfig = "tsconfig";
    /**
     * @type {string[]}
     */
    static _defaultExclude = [
        "**/.*","**/*.tmp", "**/doc*/**", "**/res*/**", "**/dist*/**", "**/test*/**", "**/script*/**", "**/node_modules/**"
    ];
    /**
     * @private
     * @type {WpwTscSvcCacheableConfig}
     */
    _;
    /**
     * @private
     * @readonly
     * @type {Readonly<WpwTsCompilerOptions>}
     */
    _compilerOptionsRo;
    /**
     * @private
     * @type {string[]}
     */
    _configFiles;
    /**
     * @private
     * @type {string}
     */
    _nodeVersion;
    /**
     * @private
     * @type {number}
     */
    _nodeVersionLtsMajor;
    /**
     * @private
     * @type {WpwTscSvcPaths}
     */
    _paths;
    /**
     * @private
     * @type {string}
     */
    _tsBuildInfoFile;
    /**
     * @private
     * @type {string}
     */
    _tsVersion;


    /**
     * @param {WpwServiceOwnerOptions} options
     */
    constructor(options)
    {
        super(applyIf({ slug: "tsc" }, options));
        this._configFiles = [];
        this._tsVersion = this._ts_.tsVersion();
        this._compilerOptionsRo = this._obj_.create();
        this._nodeVersion = this._node_.nodejsVersion();
        this._nodeVersionLtsMajor = this._node_.nodejsLtsMajor();
        this._paths = { path: "", pathRel: "", dir: "", file: "", pathRc: "" };
        this._tsBuildInfoFile = this._path_.joinPath(this.build.virtualEntry.dir, `${this.build.name}.tsbuildinfo`);
        const emptyCfg = this.getTsConfigEmpty();
        this._ = {
            dir: "", file: "", path: "", pathRel: "", ct: 0, hash: "", ts: 0, tsRc: 0, pathRc: "", dirty: false,
            configs: { file: emptyCfg, tgt: this._obj_.clone(emptyCfg), full: { ...emptyCfg, exclude: [] }}
        };
    }


    get compilerOptions() { return this._compilerOptionsRo; }
    get configFileDir() { return this._.dir; }
    get hasOptionExclude() { return this._types_.isArray(this._.configs.full.exclude, false); }
    get hasCompilerOptionPaths() { return this._types_.isObject(this._.configs.full.compilerOptions.paths, false, false); }
    get hasTsPathsOrExcludes() { return this.hasOptionExclude || this.hasCompilerOptionPaths; }
    get configFileName() { return this._.file; }
    get configFilePath() { return this._.path; }
    get configFilePathRel() { return this._.pathRel; }
    get configFileRtDir() { return this._paths.dir; }
    get configFileRtName() { return this._paths.file; }
    get configFileRtPath() { return this._paths.path; }
    get configFileRtPathRel() { return this._paths.pathRel; }
    get configFileRtIsOrig() { return this._paths.file === this._.file; }
    get configFileRtIsMock() { return this._paths.file !== this._.file && this.configFileRtIsTemp; }
    get configFileRtIsTemp() { return this._paths.file.endsWith(".tmp"); }
    get tsBuildInfoFile() { return this._tsBuildInfoFile; }
    get tsconfig() { return this._.configs.full; }
    get versionNodeLtsMajor() { return this._nodeVersionLtsMajor; }
    get versionNodeRuntime() { return this._nodeVersion; }
    get versionTypescript() { return this._tsVersion; }


	/**
     * @since 1.12.0
     * @see {@link [tsconfig.reference](https://www.typescriptlang.org/tsconfig)}
	 * @param {WpwTsCompilerOptions} [compilerOptions] strong-merged compiler to apply to the build's configured options
	 * @returns {WpwTsCompilerOptions}
	 */
	getCompilerOptions(compilerOptions) { return merge({}, this._compilerOptionsRo, compilerOptions); }


	/**
     * @since 1.12.0
     * @see {@link [tsconfig.reference](https://www.typescriptlang.org/tsconfig)}
	 * @param {WpwTsCompilerOptions} [compilerOptions] strong-merged compiler to apply to the build's configured options
	 * @returns {WpwTsCompilerOptions}
	 */
	getTranspilationCompilerOptions(compilerOptions)
    {
        return merge({}, this._compilerOptionsRo, this._.configs.tgt.compilerOptions, compilerOptions);
    }


	/**
     * @private
     * @since 1.12.0
     * @see {@link [tsconfig.reference](https://www.typescriptlang.org/tsconfig)}
	 * @param {WpwTsCompilerOptions} defCmpOpts default options read frm tsconfig.json file, this should only be
     * specified for initialization on startup
     * @param {Required<WpwSourceCodeConfig>} cfg language, specified only for initialization on startup
	 * @returns {WpwTsCompilerOptions}
     * @throws {Error}
	 */
	_getCompilerOptions(defCmpOpts, cfg)
	{
		const b = this.build,
              opts = this._obj_.clone(defCmpOpts),
              language = cfg?.language || b.sourceInfo?.language || "javascript",
              target = defCmpOpts.target || this._.configs.tgt.compilerOptions.target || "",
              allowJs = !!(cfg?.ext || b.isJs || b.isJsTs || language.includes("javascript")),
              lib = this._arr_.uniq([
                  ...this._arr_.asArray(defCmpOpts.lib), ...this._arr_.asArray(this._.configs.tgt.compilerOptions.lib)
              ]).filter(isWpwTsCompilerOptionLib);

        this._obj_.merge(opts,
        {   lib, target, allowJs, noEmit: false,
            skipLibCheck: true, noEmitOnError: true,
            inlineSources: false, declarationMap: false,
            esModuleInterop: true, inlineSourceMap: false,
            declaration: false, emitDeclarationOnly: false,
            sourceMap: b.isDevMode, maxNodeModuleJsDepth: 0,
            module: "preserve", moduleResolution: "bundler",
            useDefineForClassFields: /es2022|esnext/i.test(target),
            customConditions: !b.isWeb ? [ "node" ] : [ "browser" ],
            // jsx: defCmpOpts.jsx || (b.isReact ? "react" : undefined),
            resolvePackageJsonExports: true, resolvePackageJsonImports: true,
            useUnknownInCatchVariables: !!defCmpOpts.useUnknownInCatchVariables,
            incremental: !defCmpOpts.composite && defCmpOpts.incremental !== false,
            tsBuildInfoFile: !defCmpOpts.composite ? this._tsBuildInfoFile : undefined,
            allowSyntheticDefaultImports: true, forceConsistentCasingInFileNames: true,
            isolatedModules: defCmpOpts.isolatedModules || (b.isTs && b.loader === "babel"),
            checkJs: allowJs && !!(b.options.tscheck?.javascript?.enabled || defCmpOpts.checkJs)
        });

        if (b.isTypes)
        {   const bundle = b.options.types.bundle;
            this._obj_.cleanPrototype(opts, bundle ? "declarationDir" : "outFile");
            this._obj_.apply(opts, { declaration: true, emitDeclarationOnly: true });
            if (!bundle?.enabled) {
                opts.declarationDir = b.virtualEntry.dirBuildRelToProj;
            }
            else
            {   const outFilename = (bundle?.name || "types").replace(/\.d\.ts$/, "");
                opts.outFile = this._path_.resolvePath(b.virtualEntry.dirDistRelToProj, `${outFilename}.d.ts`);
            }
        }
        else if (!opts.jsx && language.startsWith("react"))
        {   if (language === "react.typescript") {
                this._obj_.apply(opts, { jsx: "react" });
            }
            else if (language === "react.javascript") {
                this._obj_.apply(opts, { jsx: "react-jsx", jsxFactory: "React.createElement" });
            }
            else if (language === "reactnative") {
                this._obj_.apply(opts, { jsx: "react-native", jsxFactory: "React.createElement" });
        }   }

        return this._obj_.removeEmpty(opts, { obj: true, arr: true });
	}


    /**
     * @private
     * @param {string | undefined} cfgFile
     * @param {string} [pad]
     * @returns {Promise<WpwTscSvcPaths>}
     */
    async getConfigFiles(cfgFile, pad)
    {
        const build = this.build,
              basePath = build.getBasePath(),
              ctxPath = build.getContextPath(),
              libraryTypeCjsEsm = build.isModule ? "esm" : "cjs",
              mode2 = build.mode === "development" ? "dev" : build.mode.substring(0, 4),
              fileNames = [ ...build.target, build.type, libraryTypeCjsEsm, build.mode, mode2 ],
              l = this.logger.write("locate and load associated config files for this build", 1, pad),
              rcCfgPath = cfgFile ? (isAbsPath(cfgFile) ? cfgFile : resolvePath(ctxPath, cfgFile)) : undefined,
              tsNames = this._str_.allConcatenations(fileNames, "." , [ "tsconfig", "jsconfig" ], [ "json" ], true);
        let path = "", pathRel = "", dir = build.paths.ctx;
        l.write(`   find spmhrc files starting in root dir '${dir}'`, 1, pad);

        const pathRc = await this._fs_.findExPath(
            [ ".wpwrc.json", ".wpwrc.js", ".wpwrc.yaml", ".wpwrc.yml" ], this._arr_.uniq([ basePath, ctxPath ]), true
        ) || this._path_.resolve(basePath, ".wpwrc.json");

        const searchedFiles = (await this._fs_.findFiles("**/{t,j}sconfig*.json", { cwd: dir }))
                              .filter((f) => isTsJsConfigFile(f));
        this._configFiles = searchedFiles.filter((f) => tsNames.includes(basename(f)));
        if (this._configFiles.length === 0)
        {   const fileAltNames = [ ...build.target, build.type, build.mode, mode2 ],
                  tsAltNames = this._str_.allConcatenations(fileAltNames, "." , [ "tsconfig", "jsconfig" ], [ "json" ], true);
            l.write(`   re-search ${tsAltNames.length} possible filenames with lib-type part in '${dir}'`, 1, pad);
            this._configFiles = searchedFiles.filter((f) => tsAltNames.includes(basename(f)));
            if (this._configFiles.length === 0 && dir !== build.paths.base)
            {   dir = build.getBasePath();
                l.write(`   re-search for alternate named tsconfig files in '${dir}'`, 1, pad);
                this._configFiles = (await this._fs_.findFiles("**/{t,j}sconfig.*.json", { cwd: dir }))
                                    .filter((f) => tsNames.includes(basename(f)));
                if (this._configFiles.length === 0)
                {   l.write(`   re-search ${tsAltNames.length} possible filenames with lib-type part in '${dir}'`, 1, pad);
                    this._configFiles = searchedFiles.filter((f) => tsAltNames.includes(basename(f)));
        }   }   }

        if (rcCfgPath && isTsJsConfigFile(rcCfgPath))
        {   if (existsSync(rcCfgPath))
            {   const rcCfgFile = basename(rcCfgPath);
                l.value(`   use rc-configured ${rcCfgFile.substring(0, 2)}config file`, rcCfgPath, 1, pad);
                path = rcCfgPath; pathRel = relativePath(ctxPath, rcCfgFile);
            } else
            {   const rcCfgPathRel = relativePath(ctxPath, rcCfgPath);
                build.addMessage(
                {   suggestSuppress: true,
                    module: "source", code: this.MsgCode.WARNING_RESOURCE_MISSING,
                    message: `the configured config file ${rcCfgPathRel} does not exist`,
                    detail: "an attempt will be made to search for and find a valid config file"
                });
        }   }
        else if (this._configFiles.length > 0)
        {   l.write(`   found ${this._configFiles.length} possible associated j/tsconfig files`, 1, pad);
            path = this._configFiles.sort((x, y) =>
            {   const t = `${this.build.type}.`, n = `${this.build.name}.`,
                      tgt = `${this.build.target[0]}.`, m = `${this.build.mode}.`;
                if ((x.includes(t) && !y.includes(t)) || (x.includes(n) && !y.includes(n)) ||
                    (x.includes(tgt) && !y.includes(tgt)) || (x.includes(m) && !y.includes(m)))
                { return -1; }
                if ((y.includes(t) && !x.includes(t)) || (y.includes(n) && !x.includes(n)) ||
                    (y.includes(tgt) && !x.includes(tgt)) || (y.includes(m) && !x.includes(m)))
                { return 1; }
                return x.split(".").length - y.split(".").length;
            })[0];
            if (l.level >= 3)
            {   l.write("   files ordered by likeliness", 3, pad);
                this._configFiles.forEach((f) => l.write("      " + f, 3, pad));
            }
            pathRel = path ? relativePath(dir, path) : "";
            l.write(`   set '${basename(path).substring(0, 2)}' tsconfig file to '${pathRel}'`, 1, pad);
        }
        else
        {   build.addMessage(
            {   suggestSuppress: true,
                module: "source", code: this.MsgCode.WARNING_RESOURCE_MISSING,
                message: "could not find an associated or valid tsconfig file for this build",
                detail: "a temporary configuration file will be created and used, if possible",
                suggest: "set the .wpwrc config file property '[builds|root].source.configFile' to the path" +
                         "of the appropriate j/tsconfig.*.json configuration file"
            });
        }

        return {
            path, pathRc, dir: this._path_.dirname(path), file: this._path_.basename(path), pathRel: this._path_.fwdSlash(pathRel)
        };
    }


    /**
     * @private
     * @param {string | undefined} path
     * @param {string | undefined} pathRc
     * @param {string} lPad
     * @returns {Promise<WpwTscSvcConfigFileInfo>}
     */
    async getFileConfig(path, pathRc, lPad)
    {
        const b = this.build, l = this.logger, cfg = this.getTsConfigEmpty();
        l.write("get base configuration from tsconfig file", 1, lPad);
        l.value("   tsconfig file path", path, 1, lPad);
        if (this._fs_.existsSync(path))
        {   const tscArgs = [ "-p", `./${this._path_.basename(path)}`, "--showConfig" ],
                  // result = await this.execTsc(tscArgs, this._path_.dirname(path), lPad + "   ", 45000),
                  result = await this.execTsc(tscArgs, undefined, lPad + "   ", 45000),
                  data = result.stdout.substring(result.stdout.indexOf("{"), result.stdout.lastIndexOf("}") + 1);
            l.write(`read ${data.length} ${this._str_.pluralize("byte", data.length)} from tsc stdout`, 1, lPad);
            if (data && data.length >= 2)
            {   try
                {   this._obj_.apply(cfg, this._json_.parse(data));
                    if (!this._types_.isArray(cfg.exclude, false)) {
                        cfg.exclude = this._arr_.pushUniq(cfg.exclude, ...WpwTscService._defaultExclude);
                    }
                } catch(e) { l.errorEx(e, "invalid file content", 1, lPad); }
            } else { l.warn(`empty tsconfig file content @ '${path}'`, lPad); }
        } else { l.warn(`invalid or non-existent tsconfig path @ '${path}'`, lPad); }
        return {
            configs: { file: cfg }, path, pathRc, file: this._path_.basename(path),
            dir: this._path_.dirname(path), pathRel: b.getContextPath({ psx: true, rel: true, path })
        };
    }


    /**
     * @private
     * @param {string} [lPad]
     * @returns {Promise<WpwTscSvcCacheableConfig | undefined>}
     */
    async getCachedConfig(lPad)
    {
        const l = this.logger.write("check for pre-existing configuration in cache", 1, lPad);
        const cachedCfg = this.store.get(WpwTscService._cacheKeyTsConfig);
        if (cachedCfg)
        {   const contentInfo = await this.getContentState();
            if (contentInfo)
            {   if (contentInfo.ts <= cachedCfg.ts && contentInfo.tsRc <= cachedCfg.tsRc)
                {   if (contentInfo.ct === cachedCfg.ct)
                    {   if (/* snapshot.up2date && */contentInfo.hash === cachedCfg.filesHash) {
                            l.write("   cached config found and validated", 1, lPad); return cachedCfg;
                        } else { l.write("   cached config found but source file content changed", 1, lPad); }
                    } else { l.write("   cached config found but # of source files has changed", 1, lPad); }
                } else { l.write("   cached config found but associated config files have been modified", 1, lPad); }
            } else { l.warn(`   cached config found but '${cachedCfg.path}' no longer exists`, lPad); }
        } else { l.write("   config not found in cache", 1, lPad); }
    }


    /**
     * @private
     * @param {string} [lPad]
     * @returns {Promise<WpwTscSvcContentInfo | undefined>}
     */
    async getContentState(lPad)
    {
        try
        {   if (this._fs_.existsSync(this._.path) && this._fs_.existsSync(this._.pathRc))
            {   const sourceGlob = "**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}",
                      ts = this._fs_.getDateModifiedSync(this._.path),
                      tsRc = this._fs_.getDateModifiedSync(this._.pathRc),
                      files = this._.configs.file.files ||
                              await this._fs_.findFiles(sourceGlob, { cwd: this.build.getSrcPath(), nodir: false }),
                      // snapshot = await this.build.ssCache.checkSnapshot(build.getSrcPath(), null, lPad + "   ");
                      hash = this.build.ssCache.getContentHash(Buffer.from(files.sort().join("")));
                return { ct: files.length, hash, ts, tsRc };
            }
        } catch(e) { this.logger.errorEx(e, "failed to get comtent info for cache validation", 1, lPad); }
    }


    /**
     * @param {WpwTsConfigFile} tsconfig
     * @param {WpwTsConfigFile} tsconfigRc
     * @param {string} lPad
     * @returns {WpwTsConfigFileFull}
     */
    getEnvPlatformConfig(tsconfig, tsconfigRc, lPad)
    {
        let tsPaths = tsconfig.compilerOptions.paths;
        const b = this.build, l = this.logger,
              envSrcPath = b.getSrcPath({ path: "env" }),
              cfg = applyIf(tsconfig, { files: [], exclude: [] });
        if (!this._fs_.existsSync(envSrcPath)) {
            return cfg;
        }
        const envPathAlias = ":env/*", pluralize = this._str_.pluralize.bind(this),
              nodeRgx = /env\/node(?:js)?\//i, webRgx = /env\/(?:web|browser)\//i,
              bldRgx = !b.isWeb ? nodeRgx : webRgx, bldOppRgx = b.isWeb ? nodeRgx : webRgx,
              hasOwnRcEnvPath = !!tsPaths?.[envPathAlias]?.find((p) => bldRgx.test(p)),
              hasOwnRcEnvExclude = !!tsconfigRc.exclude?.find((e) => bldOppRgx.test(e));

        l.values([
            [ "platform env src path'", envSrcPath ], [ "using tsconfig file", this._.path || "not found" ],
            [ "has rc env 'exclude'", hasOwnRcEnvExclude ], [ "has rc env compiler option 'paths'", hasOwnRcEnvPath ]
        ], 1, lPad + "   ");

        let diff = this._arr_.popBy(cfg.files, (f) => bldOppRgx.test(f)).length;
        if (diff > 0) {
            l.write(`   remove ${diff} off-env ${pluralize("path", diff)} from 'files'`, 1, lPad);
        }

        diff = this._arr_.popBy(cfg.exclude, (e) => bldRgx.test(e)).length;
        if (diff > 0) {
            l.write(`   remove ${diff} off-env ${pluralize("path", diff)} from 'exclude'`, 1, lPad);
        }
        if (!hasOwnRcEnvExclude)
        {   const pathGlob = `**/env/${!b.isWeb ? "web" : "node"}/**`;
            diff = pushUniq(cfg.exclude, pathGlob).length - cfg.exclude.length;
            if (diff !== 0) {
                l.write(`   add off-env path glob '${pathGlob}' to 'exclude'`, 1, lPad);
        }   }

        if (this._types_.isArray(tsPaths?.[envPathAlias]))
        {   diff = this._arr_.popBy(tsPaths[envPathAlias], (a) => bldOppRgx.test(a)).length;
            if (diff > 0) {
                l.write(`   remove ${diff} off-env path ${pluralize("glob", diff)} from 'paths'`, 1, lPad);
            }
        }
        if (!hasOwnRcEnvPath)
        {   let pathGlob;
            const envPath = b.getSrcPath({ rel: true, psx: true, dot: true, path: "env" });
            // envDirs = b.isWeb ? (findFilesSync("*", { cwd: envSrcPath nodir: false });
            if (!tsPaths) { cfg.compilerOptions.paths = tsPaths = {}; }
            if (!tsPaths[envPathAlias]) { tsPaths[envPathAlias] = []; }
            if (!cfg.compilerOptions.baseUrl) { cfg.compilerOptions.baseUrl = "./"; }
            if (!cfg.compilerOptions.rootDir) { cfg.compilerOptions.rootDir = "."; }
            if (!cfg.compilerOptions.baseUrl) { cfg.compilerOptions.baseUrl = envPath; }
            if (!b.isWeb)
            {   if (!tsPaths[envPathAlias].find((a) => nodeRgx.test(a)))
                {   pathGlob = this._fs_.existsSync(this._path_.resolve(envSrcPath, "node")) ? `${envPath}/node/*` :
                        (this._fs_.existsSync(this._path_.resolve(envSrcPath, "nodejs")) ? `${envPath}/nodejs/*` : "");
            }   }
            else if (!tsPaths[envPathAlias].find((a) => webRgx.test(a)))
            {   pathGlob = this._fs_.existsSync(this._path_.resolve(envSrcPath, "web")) ? `${envPath}/web/*` :
                    (this._fs_.existsSync(this._path_.resolve(envSrcPath, "browser")) ? `${envPath}/browser/*` : "");
            }
            if (pathGlob)
            {   tsPaths[envPathAlias].push(pathGlob);
                l.write(`   add env path glob '${pathGlob}' to 'paths'`, 1, lPad);
            }
        }
        return cfg;
    }


    /**
	 * @param {WpwTsCompilerOptions} [compilerOptions] compilerOptions to merge into tsconfig.compilerOptions
     * @returns {WpwTsConfigFileFile}
     */
    getTsConfig(compilerOptions)
    {
        return compilerOptions ?  merge({}, this.tsconfig, { compilerOptions }) : this._obj_.clone(this.tsconfig);
    }


    /**
     * @private
     * @param {Partial<IWpwTsConfigFile>} [obj]
     * @returns {WpwTsConfigFileFile}
     */
    getTsConfigEmpty(obj)
    {
        return this._obj_.apply({ compilerOptions: {}, files: [], include: [], exclude: [ "**/node_modules/**" ] }, obj);
    }


    /**
     * Gets defined base configurations by node major version
     *
     * @private
     * @since 1.19.6
     * @see {@link [target-config-bases](https://github.com/tasconfig/bases/tree/main)}
     * @returns {WpwTsConfigFile}
     */
    getTargetConfig()
    {
        const b = this.build, nodeMjrVersion = this._nodeVersionLtsMajor;
        /** @type {WpwTsConfigFile} */
        const tsconfig = {
            $schema: "https://www.schemastore.org/tsconfig",
            compilerOptions: {
                esModuleInterop: true, moduleResolution: "node16", skipLibCheck: true, allowSyntheticDefaultImports: true, lib: []
        }   };

        if (b.isReact)
        {   this._obj_.mergeIf(tsconfig,
            {   _version: "23.6.0",
                compilerOptions: {
                    noEmit: true, allowJs: true, jsx: "react-jsx", module: "esnext", target: "es2015", skipLibCheck: false,
                    isolatedModules: true, resolveJsonModule: true, moduleResolution: "bundler", noFallthroughCasesInSwitch: true,
                    lib: [ "dom", "dom.iterable", "esnext" ]
                }
            });
        }
        else if (b.isReactNative)
        {   this._obj_.merge(tsconfig,
            {   _version: "3.0.2",
                compilerOptions:
                {   allowJs: true, target: "esnext", module: "commonjs", jsx: "react-native", skipLibCheck: false,
                    isolatedModules: true, esModuleInterop: false, resolveJsonModule: true, moduleResolution: "node10",
                    types: [ "react-native", "jest" ], noEmit: true,
                    lib: [
                        "es2019", "es2020.bigint", "es2020.date", "es2020.number", "es2020.promise",
                        "es2020.string", "es2020.symbol.wellknown", "es2021.promise", "es2021.string",
                        "es2021.weakref", "es2022.array",  "es2022.object", "es2022.string"
                    ]
                }
            });
        }
        else if (nodeMjrVersion >= 24)
        {   this._obj_.merge(tsconfig,
            {   _version: "24.0.0",
                compilerOptions: {
                    module: "nodenext", target: "es2024",
                    lib: [ "es2024", "ESNext.Array", "ESNext.Collection", "ESNext.Iterator", "ESNext.Promise" ]
                }
            });
        }
        else if (nodeMjrVersion >= 22)
        {   this._obj_.merge(tsconfig,
            {   _version: "22.0.0",
                compilerOptions: {
                    module: "nodenext", target: "es2022",
                    lib: [ "es2024", "ESNext.Array", "ESNext.Collection", "ESNext.Iterator" ]
                }
            });
        }
        else if (nodeMjrVersion >= 20)
        {   this._obj_.merge(tsconfig,
            {   _version: "20.1.0",
                compilerOptions: {
                    module: "nodenext", target: "es2022", lib: [ "es2023" ]
                }
            });
        }
        else if (nodeMjrVersion >= 18)
        {   this._obj_.merge(tsconfig,
            {   _version: "18.2.0",
                compilerOptions: {
                    module: "node16", target: "es2022", lib: [ "es2023" ]
                }
            });
        }
        else if (nodeMjrVersion >= 16)
        {   this._obj_.merge(tsconfig,
            {   _version: "16.1.0",
                compilerOptions: {
                    module: "node16", target: "es2021", lib: [ "es2021" ]
                }
            });
        }
        else if (nodeMjrVersion >= 14)
        {   this._obj_.merge(tsconfig,
            {   _version: "14.1.0",
                compilerOptions: {
                    module: "node16", target: "es2020", lib: [ "es2020" ]
                }
            });
        }
        else if (nodeMjrVersion >= 12)
        {   this._obj_.merge(tsconfig,
            {   _version: "12.1.0",
                compilerOptions: {
                    module: "node16", target: "es2019", lib: [ "es2019", "es2020.promise", "es2020.bigint", "es2020.string" ]
                }
             });
        }
        else
        {   this._obj_.merge(tsconfig,
            {   _version: "10.0.0",
                compilerOptions: {
                    target: "es2015", module: "commonjs", moduleResolution: "node10", lib: [ "es2018" ]
                }
            });
        }

        if (b.isWeb)
        {   tsconfig.compilerOptions.target = "es2017";
            this._arr_.pushUniq(tsconfig.compilerOptions.lib, "dom");
        }
        if (this._.configs.file.compilerOptions.target) {
            tsconfig.compilerOptions.target = this._.configs.file.compilerOptions.target;
        }
        if (this._types_.isArray(this._.configs.file.compilerOptions.lib)) {
            this._arr_.pushUniq(tsconfig.compilerOptions.lib, ...this._.configs.file.compilerOptions.lib);
        }
        if (this._.configs.file.compilerOptions.module && this._.configs.file.compilerOptions.moduleResolution)
        {   if (this._.configs.file.compilerOptions.module !== "preserve") {
                tsconfig.compilerOptions.module = this._.configs.file.compilerOptions.module;
            }
            if (this._.configs.file.compilerOptions.moduleResolution !== "bundler") {
                tsconfig.compilerOptions.moduleResolution = this._.configs.file.compilerOptions.moduleResolution;
        }   }
        else if (/node1[68]/.test(this._.configs.file.compilerOptions.module || "")) {
            tsconfig.compilerOptions.moduleResolution = "node16";
        }

        return tsconfig;
    }


    /**
     * @param {Required<WpwSourceCodeConfig>} cfg
     * @param {string} lPad
     * @returns {Promise<Partial<IWpwSourceCodeInfo>>}
     * @throws {Error}
     */
    async init(cfg, lPad)
    {
        const b = this.build, l = this.logger,
              envSrcPath = b.getSrcPath({ path: "env" }), hasEnvSrc = this._fs_.existsSync(envSrcPath);
        l.values([
            [ "typescript version", this._tsVersion ], [ "node runtime version", this._nodeVersion ],
            [ "node target version.major", this._nodeVersionLtsMajor ]
        ], 1, lPad, false, "create and configure ts runtime build config");
        if (hasEnvSrc) { l.value("   platform env source path", envSrcPath, 1, lPad); }

        const cachedCfg = await this.getCachedConfig(lPad + "   ");
        if (cachedCfg && !cachedCfg.dirty)
        {   l.write("   create ts runtime profile using cached configuration", 1, lPad);
            this._obj_.apply(this._, cachedCfg);
            this._obj_.apply(this._paths, this._obj_.pick(cachedCfg, "file", "dir", "path", "pathRel", "pathRc"));
        }
        else
        {   const tgt = this.getTargetConfig(),
                  cfgFilePaths = await this.getConfigFiles(cfg.configFile, lPad + "   "),
                  mergeFn = !cfg.tsconfigMergeArr ? this._obj_.merge : this._obj_.mergeStrong;
            this._obj_.apply(this._, cfgFilePaths);
            if (this._.path)
            {  l.write(`   create new ts runtime profile using config file '${this._.file}'`, 1, lPad);
                const fileConfig = await this.getFileConfig(this._.path, this._.pathRc, lPad + "   "),
                      contentState = await this.getContentState();
                mergeFn(this._, { configs: { tgt },  ...fileConfig, ...contentState });
                this._obj_.apply(this._paths, this._obj_.pick(fileConfig, "file", "dir", "path", "pathRel", "pathRc"));
            }
            else
            {   l.write("   create temp mocked ts runtime profile, valid config file not found", 1, lPad);
                const tmpFile = `.wpw.a${++WpwTscService._tmpFileId}.tsc.tmp`,
                      tmpPath = this._path_.joinPath(b.virtualEntry.dirBuild, tmpFile);
                mergeFn(this._, {
                    ...this._paths, configs: { tgt, file: this.getTsConfigEmpty({ include: [ b.virtualEntry.dirBuild ]}) }
                });
                this._obj_.apply(this._paths, {
                    file: tmpFile, dir: b.virtualEntry.dirBuild, path: tmpPath,
                    pathRc: tmpPath.replace(tmpFile, ".spmhrc.json"), files: [],
                    pathRel: this.build.getContextPath({ psx: true, rel: true, path: tmpPath })
                });
                await this._fs_.writeFileAsync(tmpPath, this._json_.safeStringify(this._.configs.file));
            }

            const tsconfig = mergeFn(this._.configs.tgt, this._.configs.file),
                  cmpOptsRc = this._obj_.merge({}, cfg.compilerOptions),
                  tsconfigRc = this._obj_.merge({}, { compilerOptions: cmpOptsRc }, cfg.tsconfig),
                  compilerOptions = this._getCompilerOptions(tsconfig.compilerOptions, cfg);
            mergeFn(tsconfig, { compilerOptions }, { compilerOptions: cmpOptsRc }, tsconfigRc);
            this._.configs.full = this.getEnvPlatformConfig(tsconfig, tsconfigRc, lPad + "   ");
        }

        this._obj_.merge(this._compilerOptionsRo, this._.configs.full.compilerOptions);
        this._obj_.lock2({ obj: true, arr: true },
            this._.configs.full, this._.configs.file, this._.configs.tgt, this._compilerOptionsRo
        );

        await this.maybeWriteTmpConfigFile(lPad + "   ");
        await this.maybeDeleteTsBuildInfoFile(this._.dir, this._.dirty);
        if (this._.dirty) {
            await this.store.setAsync({ [WpwTscService._cacheKeyTsConfig]: this._obj_.apply(this._, { dirty: false }) });
        }

        l.values([
            [ "# of config files found in project", this._configFiles.length ],
            [ "# of source files included in build", this._.configs.full.files.length ],
            [ "finalized ts configuration", this._json_.safeStringify(this._.configs.full) ]
        ], 1, lPad + "   ", false, ("valid base config found"));
        l.write("completed configuration of ts runtime build config", 2, lPad);
        return { ...this._paths };
    }


    /**
     * @private
     * @param {string} dir
     * @param {boolean} [cacheCfgInvalid]
     */
	async maybeDeleteTsBuildInfoFile(dir, cacheCfgInvalid)
	{
		const distPath = this.build.getDistPath(), vBuildPath = this.build.virtualEntry.dir;
		if (this._tsBuildInfoFile && (cacheCfgInvalid || (!existsSync(vBuildPath) || !existsSync(distPath))))
		{   const bInfoFile = isAbsPath(this._tsBuildInfoFile) ? this._tsBuildInfoFile :
                              resolvePath(dir || this.build.getContextPath(), this._tsBuildInfoFile);
			try
            {   await deleteFile(bInfoFile);
                this.logger.write("   deleted tsbuildinfo file", 2);
			}
			catch(e)
			{   this.build.addMessage({
					exception: e,
					code: this.MsgCode.WARNING_RESOURCE_UNLINK_FAILED,
					message: `unable to delete 'tsbuildinfo' file @ '${bInfoFile}'`,
					suggest: `attempt to manually delete '${bInfoFile}', or perform a system reboot`
				});
			}
		}
	}


    /**
     * @private
     * @param {string} lPad
     */
    async maybeWriteTmpConfigFile(lPad)
    {
        const b = this.build,
              full = this._.configs.full, file = this._.configs.file, stringify = this._json_.safeStringify,
              rtConfigIsMod = (full.exclude && !file.exclude) || (!full.exclude && file.exclude) ||
                  (stringify(file.exclude) !== stringify(full.exclude)) ||
                  (stringify(file.compilerOptions.paths) !== stringify(full.compilerOptions.paths));
        if (rtConfigIsMod)
        {   const buildTypeNeedsTmpCfg = b.options.tscheck?.javascript?.enabled ||
                  (b.isTypes && (b.options.types.method === "tsc" ||
                   b.options.types.bundle?.enabled && b.options.types.bundle.bundler === "tsc"));
            if (buildTypeNeedsTmpCfg)
            {   const needTmpCfg = !([ ...this._arr_.asArray(b.target), `${b.library}`, b.type, b.mode ]
                      .map((f) => new RegExp(`[jt]sconfig\\.(?:[\\w-]+\\.)?${f}[\\w-]*\\.`))
                      .find((rgx) =>
                          rgx.test(this._.file) || (this._configFiles.length > 1 &&
                          this._configFiles.find((p) => rgx.test(this._path_.basename(p))) &&
                          this.build.wrapper.buildConfigs.filter((c) => !c.disabled).length > 1)
                      ));
                if (needTmpCfg)
                {  const file = `.wpw.b${++WpwTscService._tmpFileId}.tsc.tmp`, //  "tsconfig.wpw.json"
                         path = this._path_.resolve(this._paths.dir, file),
                         pathRel = this._paths.pathRel.replace(this._paths.file, file);
                    this._obj_.apply(this._paths, { file, path, pathRel });
                    this.logger.write(`write temp tsconfig file '${file}' @ '${this._paths.dir}'`, 1, lPad);
                    await this._fs_.writeFileAsync(path, this._json_.safeStringify(this._.configs.full));
                }
            }
        }
    }


	/**
	 * @param {string[]} args
     * @param {string} [cwd]
	 * @param {string} [lPad]
     * @param {number} [timeout]
	 * @returns {Promise<WpwExecResult | undefined>}
	 */
	execTsc(args, cwd, lPad = "   ", timeout = 30000)
	{
		return this.exec(`npx tsc ${args.join(" ")}`, "tsc", true, {
            timeout, logPad: lPad, execOptions: { cwd: cwd || this.build.getContextPath(), encoding: "utf8" }
        });
	}
}


module.exports = WpwTscService;