How to compile tsconfig.json into a config object using TypeScript API?
Asked Answered
G

3

9

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.
Galleywest answered 13/6, 2021 at 9:29 Comment(1)
Related: github.com/microsoft/TypeScript/issues/44573Galleywest
T
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 [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:

  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": []
}
Trilingual answered 16/6, 2021 at 18:24 Comment(1)
Very thorough write up! I'm finding that parseJsonConfigFileContent resolves extends though. eg. https://mcmap.net/q/696305/-how-to-get-compileroptions-from-tsconfig-jsonPhiphenomenon
H
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.

Harvestman answered 18/11, 2021 at 0:11 Comment(0)
S
-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);
}
Shannashannah answered 13/6, 2021 at 10:41 Comment(4)
This won't work if the config file is an invalid JSON (and it might be due to comments and trailing commas)Galleywest
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 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.Shannashannah
github.com/microsoft/TypeScript/issues/4987Galleywest
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.Shannashannah

© 2022 - 2024 — McMap. All rights reserved.