Edit
In a comment below, @Hiroki Osame explains that this answer by using ts.parseJsonConfigFileContent
he was able to get the extends
followed automatically without any "hand-crafting".
Also on this page here, @Simon Buchan's answer looks to be similarly correct.
Short Answer
A function to read compiler options from a tsconfig file while correctly handling tsconfig extends
keyword inheritance
function getCompilerOptionsJSONFollowExtends(filename: string): {[key: string]: any} {
let compopts = {};
const config = ts.readConfigFile(filename, ts.sys.readFile).config;
if (config.extends) {
const rqrpath = require.resolve(config.extends);
compopts = getCompilerOptionsJSONFollowExtends(rqrpath);
}
return {
...compopts,
...config.compilerOptions,
};
}
The result of that can be converted to type ts.CompilerOptions
via
const jsonCompopts = getCompilerOptionsJSONFollowExtends('tsconfig.json')
const tmp = ts.convertCompilerOptionsFromJson(jsonCompopts,'')
if (tmp.errors.length>0) throw new Error('...')
const tsCompopts:ts.CompilerOptions = tmp.options
TL;DR
These related functions exist in [email protected]
:
ts.readConfigFile
ts.parseConfigFileTextToJson
ts.convertCompilerOptionsFromJson
ts.parseJsonConfigFileContent
ts.parseJsonSourceFileConfigFileContent
This post only addresses the first three:
ts.readConfigFile
console.log(
JSON.stringify(
ts.readConfigFile('./tsconfig.base.json', ts.sys.readFile),
null,
2
)
);
where tsconfig.base.json
has content
{
"extends": "@tsconfig/node14/tsconfig.json",
//comment
"compilerOptions": {
"declaration": true,
"skipLibCheck": true,
"sourceMap": true,
"lib": ["es2020"],// trailing comma
}
}
results in
{
"config": {
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"declaration": true,
"skipLibCheck": true,
"sourceMap": true,
"lib": [
"es2020"
]
}
}
}
The things to notice here:
- The config file referenced by extends is not pulled in and expanded.
- The compiler options are not converted into the internal form required by typescript compiler API functions. (Not of type
ts.CompilerOptions
)
- Comments are stripped and trailing commas ignored.
ts.parseConfigFileTextToJson
const parsed2 = ts.parseConfigFileTextToJson(
''/*'./tsconfig.base.json'*/, `
{
"extends": "@tsconfig/node14/tsconfig.json",
// comment
"compilerOptions": {
"declaration": true,
"skipLibCheck": true,
"sourceMap": true,
"lib": ["es2020"], // trailing comma
}
}
`);
console.log(JSON.stringify(parsed2, null, 2));
results in
{
"config": {
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"declaration": true,
"skipLibCheck": true,
"sourceMap": true,
"lib": [
"es2020"
]
}
}
}
The function is the same as ts.readConfigFile
except that text is
passed instead of a filename.
Note: The first argument (filename) is ignored unless perhaps there is an error. Adding a real filename but leaving the second argument empty results in empty output. This function can not read in files.
ts.convertCompilerOptionsFromJson
const parsed1 = ts.convertCompilerOptionsFromJson(
{
lib: ['es2020'],
module: 'commonjs',
target: 'es2020',
},
''
);
console.log(JSON.stringify(parsed1, null, 2));
results in
{
"options": {
"lib": [
"lib.es2020.d.ts"
],
"module": 1,
"target": 7
},
"errors": []
}
The value of the options
property of the result is in the internal format required by typescript compiler API. (I.e. it is of type ts.CompilerOptions
)
The value (1) of module
is actually the compiled value of ts.ModuleKind.CommonJS
, and the value (7) of target
is actually the compiled value of ts.ScriptTarget.ES2020
.
discussion / extends
When extends
keyword does NOT come into play then
by using the following functions:
ts.readConfigFile
ts.convertCompilerOptionsFromJson
as shown above, you should be able to get what you want.
However, when the extends
keyword DOES come into play, it is more complicated. I can find no existing API function to follow extends automatically.
There is, however, a CLI function to do so
npx tsc -p tsconfig.base.json --showConfig
results in
{
"compilerOptions": {
"lib": [
"es2020"
],
"module": "commonjs",
"target": "es2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
},
"files": [
"./archive/doc-generator.ts",
"./archive/func-params-exp.ts",
"./archive/reprinting.ts",
"./archive/sw.ts",
....
....
]
}
where all the files implicitly included are also output.
The following one liner in bash will yield just the compile options -
echo 'console.log(JSON.stringify(JSON.parse('\'`npx tsc -p tsconfig.base.json --showConfig`\'').compilerOptions,null,2))' | node
results in just the compile options
{
"lib": [
"es2020"
],
"module": "commonjs",
"target": "es2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
}
Obviously, invoking CLI from a program is far from ideal.
how to follow extends using API
Show the principle:
const config1 = ts.readConfigFile('./tsconfig.base.json', ts.sys.readFile).config
console.log(JSON.stringify(config1,null,2))
const tsrpath = ts.sys.resolvePath(config1.extends)
console.log(tsrpath)
const rqrpath = require.resolve(config1.extends)
console.log(rqrpath)
const config2 = ts.readConfigFile(rqrpath, ts.sys.readFile).config
console.log(JSON.stringify(config2,null,2))
results in
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"declaration": true,
"skipLibCheck": true,
"sourceMap": true,
"lib": [
"es2020"
]
}
}
/mnt/common/github/tscapi/@tsconfig/node14/tsconfig.json
/mnt/common/github/tscapi/node_modules/@tsconfig/node14/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 14",
"compilerOptions": {
"lib": [
"es2020"
],
"module": "commonjs",
"target": "es2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Note that require.resolve
resolves to what we want, but ts.sys.resolve
does not.
Here is a function which returns compiler option correctly inheriting from extends:
function getCompileOptionsJSONFollowExtends(filename: string): {[key: string]: any} {
let compopts: ts.CompilerOptions = {};
const config = ts.readConfigFile(filename, ts.sys.readFile).config;
if (config.extends) {
const rqrpath = require.resolve(config.extends);
compopts = getCompileOptionsJSONFollowExtends(rqrpath);
}
compopts = {
...compopts,
...config.compilerOptions,
};
return compopts;
}
Test run -
const jsonCompopts = getCompileOptionsJSONFollowExtends('./tsconfig.base.json')
console.log(JSON.stringify(jsonCompopts,null,2))
const tsCompopts = ts.convertCompilerOptionsFromJson(jsonCompopts,'')
console.log(JSON.stringify(tsCompopts,null,2))
console.log('');
results in
{
"lib": [
"es2020"
],
"module": "commonjs",
"target": "es2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
}
{
"options": {
"lib": [
"lib.es2020.d.ts"
],
"module": 1,
"target": 7,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
},
"errors": []
}