8

I am looking for a public official TypeScript API (ideally, of version 4.x) for parsing tsconfig.json / jsconfig.json files into complete TypeScript configuration objects (by "complete" I mean, all of its properties are filled in with values, – either taken explicitly from the *.json, or implicitly from known defaults), as in:

import ts from "typescript";

const tsconfig = await ts.parseConfigFile('./tsconfig.json');

tsconfig.compilerOptions.noEmit;
// false
// (or whatever it is set to in *.json)

Is there such kind of function in typescript npm package? I couldn't find it myself, both in docs and in IntelliSense suggestions (the ones invoked by Ctrl + Space).

I have seen another question with the same kind of request, but a) it is unanswered, b) the link in the comments references a 5.5-year-old solution, – there's gotta be a change in API since then.


In reality, I need only a handful of properties from the config file, so it would be relatively easy to write a couple of helpers to parse the *.json file and grab its contents. However:

  • there are more exotic use cases (such as using Project References and/or extending parent config object), in which such a solution would not work;
  • the aforementioned *.json objects might have comments and trailing commas, so it is not trivial to even parse the file in the first place;
  • it would be weird to rewrite something that is known to be written already.
Parzh from Ukraine
  • 7,999
  • 3
  • 34
  • 65

3 Answers3

13

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:

  1. The config file referenced by extends is not pulled in and expanded.
  2. The compiler options are not converted into the internal form required by typescript compiler API functions. (Not of type ts.CompilerOptions)
  3. 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": []
}
Craig Hicks
  • 2,199
  • 20
  • 35
  • 3
    Very thorough write up! I'm finding that `parseJsonConfigFileContent` resolves extends though. eg. https://stackoverflow.com/a/53898219/911407 – Hiroki Osame Jan 12 '22 at 01:35
3

Somewhat confusingly, you should use ts.getParsedCommandLineOfConfigFile():

> ts.getParsedCommandLineOfConfigFile('tsconfig.json', {}, ts.sys)
{
  options: {
    moduleResolution: 2,
    module: 99,
    target: 6,
    lib: [ 'lib.es2019.d.ts' ],
    types: [ 'node' ],
    strict: true,
    sourceMap: true,
    esModuleInterop: true,
    importsNotUsedAsValues: 2,
    importHelpers: true,
    incremental: true,
    composite: true,
    skipLibCheck: true,
    noEmit: true,
    configFilePath: 'C:/code/.../tsconfig.json'
  },
  watchOptions: undefined,
  fileNames: [
    'C:/code/.../src/index.tsx',
...

The third parameter is actually a ts.ParseConfigFileHost, so you should probably manually implement that (using implementations from ts.sys)

You can also use ts.parseJsonFileContent(tsconfigContent, ts.sys, baseDir, {}, errorMessageFileName) if you have already parsed the config, for example, when it's inline in some larger config file.

Simon Buchan
  • 12,707
  • 2
  • 48
  • 55
-1

I might misunderstand your problem, but should it not suffice to load the content tsconfig.json file and parse it with JSON.parse?

const fs = require('fs');

const txt = fs.readFileSync('./tsconfig.json');
try {
   const obj = JSON.parse(txt);
   console.log(obj);
} catch (e) {
   console.error(e);
}
doberkofler
  • 9,511
  • 18
  • 74
  • 126
  • This won't work if the config file is an invalid JSON (and it might be due to comments and trailing commas) – Parzh from Ukraine Jun 13 '21 at 10:42
  • Well, standard JSON must be valid and does not support comments. If you want to support comments, you can use a custom JSON parser like https://www.npmjs.com/package/comment-json if you want to allow comments. If you want to check for errors you might just use a try/catch. – doberkofler Jun 13 '21 at 10:48
  • https://github.com/microsoft/TypeScript/issues/4987 – Parzh from Ukraine Jun 13 '21 at 10:49
  • 1
    If you question is specific to the tsconfig parser build into TypeScript, I've misunderstood. I only remember the `parseConfigFileTextToJson` function that you already used. – doberkofler Jun 13 '21 at 11:08