298 lines
8.7 KiB
Plaintext
298 lines
8.7 KiB
Plaintext
import path from 'node:path';
|
|
import {
|
|
makePromise,
|
|
loadTS,
|
|
native2posix,
|
|
resolveReferencedTSConfigFiles,
|
|
resolveSolutionTSConfig,
|
|
resolveTSConfigJson
|
|
} from './util.js';
|
|
import { findNative } from './find-native.js';
|
|
|
|
const notFoundResult = {
|
|
tsconfigFile: null,
|
|
tsconfig: {},
|
|
result: null
|
|
};
|
|
|
|
/**
|
|
* parse the closest tsconfig.json file with typescript native functions
|
|
*
|
|
* You need to have `typescript` installed to use this
|
|
*
|
|
* @param {string} filename - path to a tsconfig .json or a source file (absolute or relative to cwd)
|
|
* @param {import('./public.d.ts').TSConfckParseNativeOptions} [options] - options
|
|
* @returns {Promise<import('./public.d.ts').TSConfckParseNativeResult>}
|
|
* @throws {TSConfckParseNativeError}
|
|
*/
|
|
export async function parseNative(filename, options) {
|
|
/** @type {import('./cache.js').TSConfckCache} */
|
|
const cache = options?.cache;
|
|
if (cache?.hasParseResult(filename)) {
|
|
return cache.getParseResult(filename);
|
|
}
|
|
const {
|
|
resolve,
|
|
reject,
|
|
/** @type {Promise<import('./public.d.ts').TSConfckParseNativeResult>}*/
|
|
promise
|
|
} = makePromise();
|
|
cache?.setParseResult(filename, promise);
|
|
try {
|
|
const tsconfigFile =
|
|
(await resolveTSConfigJson(filename, cache)) || (await findNative(filename, options));
|
|
if (!tsconfigFile) {
|
|
resolve(notFoundResult);
|
|
return promise;
|
|
}
|
|
/** @type {import('./public.d.ts').TSConfckParseNativeResult} */
|
|
let result;
|
|
if (filename !== tsconfigFile && cache?.hasParseResult(tsconfigFile)) {
|
|
result = await cache.getParseResult(tsconfigFile);
|
|
} else {
|
|
const ts = await loadTS();
|
|
result = await parseFile(tsconfigFile, ts, options, filename === tsconfigFile);
|
|
await parseReferences(result, ts, options);
|
|
cache?.setParseResult(tsconfigFile, Promise.resolve(result));
|
|
}
|
|
//@ts-ignore
|
|
resolve(resolveSolutionTSConfig(filename, result));
|
|
return promise;
|
|
} catch (e) {
|
|
reject(e);
|
|
return promise;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} tsconfigFile
|
|
* @param {any} ts
|
|
* @param {import('./public.d.ts').TSConfckParseNativeOptions} [options]
|
|
* @param {boolean} [skipCache]
|
|
* @returns {import('./public.d.ts').TSConfckParseNativeResult}
|
|
*/
|
|
function parseFile(tsconfigFile, ts, options, skipCache) {
|
|
const cache = options?.cache;
|
|
if (!skipCache && cache?.hasParseResult(tsconfigFile)) {
|
|
return cache.getParseResult(tsconfigFile);
|
|
}
|
|
const posixTSConfigFile = native2posix(tsconfigFile);
|
|
const { parseJsonConfigFileContent, readConfigFile, sys } = ts;
|
|
const { config, error } = readConfigFile(posixTSConfigFile, sys.readFile);
|
|
if (error) {
|
|
throw new TSConfckParseNativeError(error, tsconfigFile, null);
|
|
}
|
|
|
|
const host = {
|
|
useCaseSensitiveFileNames: false,
|
|
readDirectory: sys.readDirectory,
|
|
fileExists: sys.fileExists,
|
|
readFile: sys.readFile
|
|
};
|
|
|
|
if (options?.ignoreSourceFiles) {
|
|
config.files = [];
|
|
config.include = [];
|
|
}
|
|
const nativeResult = parseJsonConfigFileContent(
|
|
config,
|
|
host,
|
|
path.dirname(posixTSConfigFile),
|
|
undefined,
|
|
posixTSConfigFile
|
|
);
|
|
checkErrors(nativeResult, tsconfigFile);
|
|
|
|
/** @type {import('./public.d.ts').TSConfckParseNativeResult} */
|
|
const result = {
|
|
tsconfigFile,
|
|
tsconfig: result2tsconfig(nativeResult, ts),
|
|
result: nativeResult
|
|
};
|
|
if (!skipCache) {
|
|
cache?.setParseResult(tsconfigFile, Promise.resolve(result));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {import('./public.d.ts').TSConfckParseNativeResult} result
|
|
* @param {any} ts
|
|
* @param {import('./public.d.ts').TSConfckParseNativeOptions} [options]
|
|
*/
|
|
async function parseReferences(result, ts, options) {
|
|
if (!result.tsconfig.references) {
|
|
return;
|
|
}
|
|
const referencedFiles = resolveReferencedTSConfigFiles(result, options);
|
|
result.referenced = await Promise.all(
|
|
referencedFiles.map((file) => parseFile(file, ts, options))
|
|
);
|
|
result.referenced.forEach((ref) => (ref.solution = result));
|
|
}
|
|
|
|
/**
|
|
* check errors reported by parseJsonConfigFileContent
|
|
*
|
|
* ignores errors related to missing input files as these may happen regularly in programmatic use
|
|
* and do not affect the config itself
|
|
*
|
|
* @param {any} nativeResult - native typescript parse result to check for errors
|
|
* @param {string} tsconfigFile
|
|
* @throws {TSConfckParseNativeError} for critical error
|
|
*/
|
|
function checkErrors(nativeResult, tsconfigFile) {
|
|
const ignoredErrorCodes = [
|
|
// see https://github.com/microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json
|
|
18002, // empty files list
|
|
18003 // no inputs
|
|
];
|
|
const criticalError = nativeResult.errors?.find(
|
|
(error) => error.category === 1 && !ignoredErrorCodes.includes(error.code)
|
|
);
|
|
if (criticalError) {
|
|
throw new TSConfckParseNativeError(criticalError, tsconfigFile, nativeResult);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* convert the result of `parseJsonConfigFileContent` to a tsconfig that can be parsed again
|
|
*
|
|
* - use merged compilerOptions
|
|
* - strip prefix and postfix of compilerOptions.lib
|
|
* - convert enum values back to string
|
|
*
|
|
* @param {any} result
|
|
* @param {any} ts typescript
|
|
* @returns {object} tsconfig with merged compilerOptions and enums restored to their string form
|
|
*/
|
|
function result2tsconfig(result, ts) {
|
|
// dereference result.raw so changes below don't modify original
|
|
const tsconfig = JSON.parse(JSON.stringify(result.raw));
|
|
// for some reason the extended compilerOptions are not available in result.raw but only in result.options
|
|
// and contain an extra fields 'configFilePath' and 'pathsBasePath'. Use everything but those 2
|
|
const ignoredOptions = ['configFilePath', 'pathsBasePath'];
|
|
if (result.options && Object.keys(result.options).some((o) => !ignoredOptions.includes(o))) {
|
|
tsconfig.compilerOptions = {
|
|
...result.options
|
|
};
|
|
for (const ignored of ignoredOptions) {
|
|
delete tsconfig.compilerOptions[ignored];
|
|
}
|
|
}
|
|
|
|
const compilerOptions = tsconfig.compilerOptions;
|
|
if (compilerOptions) {
|
|
if (compilerOptions.lib != null) {
|
|
// remove lib. and .dts from lib.es2019.d.ts etc
|
|
compilerOptions.lib = compilerOptions.lib.map((x) =>
|
|
x.replace(/^lib\./, '').replace(/\.d\.ts$/, '')
|
|
);
|
|
}
|
|
const enumProperties = [
|
|
{ name: 'importsNotUsedAsValues', enumeration: ts.ImportsNotUsedAsValues },
|
|
{ name: 'module', enumeration: ts.ModuleKind },
|
|
{
|
|
name: 'moduleResolution',
|
|
enumeration: {
|
|
...ts.ModuleResolutionKind,
|
|
2: 'node' /*ts.ModuleResolutionKind uses "Node10" but in tsconfig it is just node"*/
|
|
}
|
|
},
|
|
{
|
|
name: 'newLine',
|
|
enumeration: { 0: 'crlf', 1: 'lf' } /*ts.NewLineKind uses different names*/
|
|
},
|
|
{ name: 'target', enumeration: ts.ScriptTarget }
|
|
];
|
|
for (const prop of enumProperties) {
|
|
if (compilerOptions[prop.name] != null && typeof compilerOptions[prop.name] === 'number') {
|
|
compilerOptions[prop.name] = prop.enumeration[compilerOptions[prop.name]].toLowerCase();
|
|
}
|
|
}
|
|
if (compilerOptions.target === 'latest') {
|
|
compilerOptions.target = 'esnext'; // why ts, why?
|
|
}
|
|
}
|
|
|
|
// merged watchOptions
|
|
if (result.watchOptions) {
|
|
tsconfig.watchOptions = {
|
|
...result.watchOptions
|
|
};
|
|
}
|
|
|
|
const watchOptions = tsconfig.watchOptions;
|
|
if (watchOptions) {
|
|
const enumProperties = [
|
|
{ name: 'watchFile', enumeration: ts.WatchFileKind },
|
|
{ name: 'watchDirectory', enumeration: ts.WatchDirectoryKind },
|
|
{ name: 'fallbackPolling', enumeration: ts.PollingWatchKind }
|
|
];
|
|
for (const prop of enumProperties) {
|
|
if (watchOptions[prop.name] != null && typeof watchOptions[prop.name] === 'number') {
|
|
const enumVal = prop.enumeration[watchOptions[prop.name]];
|
|
watchOptions[prop.name] = enumVal.charAt(0).toLowerCase() + enumVal.slice(1);
|
|
}
|
|
}
|
|
}
|
|
if (tsconfig.compileOnSave === false) {
|
|
// ts adds this property even if it isn't present in the actual config
|
|
// delete if it is false to match content of tsconfig
|
|
delete tsconfig.compileOnSave;
|
|
}
|
|
return tsconfig;
|
|
}
|
|
|
|
export class TSConfckParseNativeError extends Error {
|
|
/**
|
|
*
|
|
* @param {TSDiagnosticError} diagnostic - diagnostics of ts
|
|
* @param {string} tsconfigFile - file that errored
|
|
* @param {any?} result - parsed result, if any
|
|
*/
|
|
constructor(diagnostic, tsconfigFile, result) {
|
|
super(diagnostic.messageText);
|
|
// Set the prototype explicitly.
|
|
Object.setPrototypeOf(this, TSConfckParseNativeError.prototype);
|
|
this.name = TSConfckParseNativeError.name;
|
|
this.code = `TS ${diagnostic.code}`;
|
|
this.diagnostic = diagnostic;
|
|
this.result = result;
|
|
this.tsconfigFile = tsconfigFile;
|
|
}
|
|
|
|
/**
|
|
* code of typescript diagnostic, prefixed with "TS "
|
|
* @type {string}
|
|
*/
|
|
code;
|
|
|
|
/**
|
|
* full ts diagnostic that caused this error
|
|
* @type {TSDiagnosticError}
|
|
*/
|
|
diagnostic;
|
|
|
|
/**
|
|
* absolute path of tsconfig file where the error happened
|
|
* @type {string}
|
|
*/
|
|
tsconfigFile;
|
|
|
|
/**
|
|
* native result if present, contains all errors in result.errors
|
|
* @type {any|undefined}
|
|
*/
|
|
result;
|
|
}
|
|
|
|
/** @typedef TSDiagnosticError {
|
|
code: number;
|
|
category: number;
|
|
messageText: string;
|
|
start?: number;
|
|
} TSDiagnosticError */
|