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