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 typescript@4.3.2:
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": []
}