diff --git a/example/inherited/tsconfig.base.json b/example/inherited/tsconfig.base.json new file mode 100644 index 0000000..ae070bb --- /dev/null +++ b/example/inherited/tsconfig.base.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@": [] + } + } +} diff --git a/example/inherited/tsconfig.json b/example/inherited/tsconfig.json new file mode 100644 index 0000000..e8c7ec8 --- /dev/null +++ b/example/inherited/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.preset.json" +} diff --git a/example/inherited/tsconfig.preset.json b/example/inherited/tsconfig.preset.json new file mode 100644 index 0000000..ffcbb94 --- /dev/null +++ b/example/inherited/tsconfig.preset.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.base.json" +} diff --git a/package.json b/package.json index 5a4f1e5..e53d584 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,10 @@ "typescript": "^4.5.2" }, "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "minimist": "^1.2.6" + }, + "peerDependencies": { + "typescript": "^4" }, "scripts": { "start": "cd src && ts-node index.ts", diff --git a/src/__tests__/tsconfig-loader.test.ts b/src/__tests__/tsconfig-loader.test.ts index 2564142..c183d60 100644 --- a/src/__tests__/tsconfig-loader.test.ts +++ b/src/__tests__/tsconfig-loader.test.ts @@ -1,9 +1,9 @@ import { - loadTsconfig, + // loadTsconfig, tsConfigLoader, walkForTsConfig, } from "../tsconfig-loader"; -import { join } from "path"; +import { join, resolve } from "path"; describe("tsconfig-loader", () => { it("should find tsconfig in cwd", () => { @@ -167,172 +167,18 @@ describe("walkForTsConfig", () => { }); }); -describe("loadConfig", () => { - it("It should load a config", () => { - const config = { compilerOptions: { baseUrl: "hej" } }; - const res = loadTsconfig( - "/root/dir1/tsconfig.json", - (path) => path === "/root/dir1/tsconfig.json", - (_) => JSON.stringify(config) - ); - expect(res).toStrictEqual(config); - }); - - it("It should load a config with comments", () => { - const config = { compilerOptions: { baseUrl: "hej" } }; - const res = loadTsconfig( - "/root/dir1/tsconfig.json", - (path) => path === "/root/dir1/tsconfig.json", - (_) => `{ - // my comment - "compilerOptions": { - "baseUrl": "hej" - } - }` - ); - expect(res).toStrictEqual(config); - }); - - it("It should load a config with trailing commas", () => { - const config = { compilerOptions: { baseUrl: "hej" } }; - const res = loadTsconfig( - "/root/dir1/tsconfig.json", - (path) => path === "/root/dir1/tsconfig.json", - (_) => `{ - "compilerOptions": { - "baseUrl": "hej", - }, - }` - ); - expect(res).toStrictEqual(config); - }); - - it("It should throw an error including the file path when encountering invalid JSON5", () => { - expect(() => - loadTsconfig( - "/root/dir1/tsconfig.json", - (path) => path === "/root/dir1/tsconfig.json", - (_) => `{ - "compilerOptions": { - }` - ) - ).toThrowError( - "/root/dir1/tsconfig.json is malformed JSON5: invalid end of input at 3:12" - ); - }); - - it("It should load a config with extends and overwrite all options", () => { - const firstConfig = { - extends: "../base-config.json", - compilerOptions: { baseUrl: "kalle", paths: { foo: ["bar2"] } }, - }; - const firstConfigPath = join("/root", "dir1", "tsconfig.json"); - const baseConfig = { - compilerOptions: { - baseUrl: "olle", - paths: { foo: ["bar1"] }, - strict: true, - }, - }; - const baseConfigPath = join("/root", "base-config.json"); - const res = loadTsconfig( - join("/root", "dir1", "tsconfig.json"), - (path) => path === firstConfigPath || path === baseConfigPath, - (path) => { - if (path === firstConfigPath) { - return JSON.stringify(firstConfig); - } - if (path === baseConfigPath) { - return JSON.stringify(baseConfig); - } - return ""; - } - ); - - expect(res).toEqual({ - extends: "../base-config.json", - compilerOptions: { - baseUrl: "kalle", - paths: { foo: ["bar2"] }, - strict: true, - }, - }); - }); - - it("It should load a config with extends from node_modules and overwrite all options", () => { - const firstConfig = { - extends: "my-package/base-config.json", - compilerOptions: { baseUrl: "kalle", paths: { foo: ["bar2"] } }, - }; - const firstConfigPath = join("/root", "dir1", "tsconfig.json"); - const baseConfig = { - compilerOptions: { - baseUrl: "olle", - paths: { foo: ["bar1"] }, - strict: true, - }, - }; - const baseConfigPath = join( - "/root", - "dir1", - "node_modules", - "my-package", - "base-config.json" - ); - const res = loadTsconfig( - join("/root", "dir1", "tsconfig.json"), - (path) => path === firstConfigPath || path === baseConfigPath, - (path) => { - if (path === firstConfigPath) { - return JSON.stringify(firstConfig); - } - if (path === baseConfigPath) { - return JSON.stringify(baseConfig); - } - return ""; - } - ); - - expect(res).toEqual({ - extends: "my-package/base-config.json", - compilerOptions: { - baseUrl: "kalle", - paths: { foo: ["bar2"] }, - strict: true, - }, +describe("loadSyncDefault", () => { + it("should result multiple levels of tsconfig extension", () => { + const cwd = resolve(__dirname, "../../example/inherited"); + const result = tsConfigLoader({ + cwd, + getEnv: (_: string) => undefined, }); - }); - - it("Should use baseUrl relative to location of extended tsconfig", () => { - const firstConfig = { compilerOptions: { baseUrl: "." } }; - const firstConfigPath = join("/root", "first-config.json"); - const secondConfig = { extends: "../first-config.json" }; - const secondConfigPath = join("/root", "dir1", "second-config.json"); - const thirdConfig = { extends: "../second-config.json" }; - const thirdConfigPath = join("/root", "dir1", "dir2", "third-config.json"); - const res = loadTsconfig( - join("/root", "dir1", "dir2", "third-config.json"), - (path) => - path === firstConfigPath || - path === secondConfigPath || - path === thirdConfigPath, - (path) => { - if (path === firstConfigPath) { - return JSON.stringify(firstConfig); - } - if (path === secondConfigPath) { - return JSON.stringify(secondConfig); - } - if (path === thirdConfigPath) { - return JSON.stringify(thirdConfig); - } - return ""; - } - ); - expect(res).toEqual({ - extends: "../second-config.json", - compilerOptions: { baseUrl: join("..", "..") }, + expect(result).toEqual({ + baseUrl: undefined, + paths: { "@": [] }, + tsConfigPath: resolve(cwd, "tsconfig.json"), }); }); }); diff --git a/src/tsconfig-loader.ts b/src/tsconfig-loader.ts index 74b7ba9..4d11e8d 100644 --- a/src/tsconfig-loader.ts +++ b/src/tsconfig-loader.ts @@ -1,21 +1,6 @@ import * as path from "path"; import * as fs from "fs"; -// eslint-disable-next-line @typescript-eslint/no-require-imports -import JSON5 = require("json5"); -// eslint-disable-next-line @typescript-eslint/no-require-imports -import StripBom = require("strip-bom"); - -/** - * Typing for the parts of tsconfig that we care about - */ -export interface Tsconfig { - extends?: string; - compilerOptions?: { - baseUrl?: string; - paths?: { [key: string]: Array }; - strict?: boolean; - }; -} +import * as typescript from "typescript"; export interface TsConfigLoaderResult { tsConfigPath: string | undefined; @@ -47,7 +32,7 @@ export function tsConfigLoader({ return loadResult; } -function loadSyncDefault( +export function loadSyncDefault( cwd: string, filename?: string, baseUrl?: string @@ -63,14 +48,23 @@ function loadSyncDefault( paths: undefined, }; } - const config = loadTsconfig(configPath); + const rawConfig = typescript.readConfigFile( + configPath, + typescript.sys.readFile + ); + + const config = typescript.parseJsonConfigFileContent( + rawConfig.config, + typescript.sys, + cwd, + {}, + filename + ); return { tsConfigPath: configPath, - baseUrl: - baseUrl || - (config && config.compilerOptions && config.compilerOptions.baseUrl), - paths: config && config.compilerOptions && config.compilerOptions.paths, + baseUrl: baseUrl || config.options.baseUrl, + paths: config.options.paths, }; } @@ -111,70 +105,3 @@ export function walkForTsConfig( return walkForTsConfig(parentDirectory, readdirSync); } - -export function loadTsconfig( - configFilePath: string, - // eslint-disable-next-line no-shadow - existsSync: (path: string) => boolean = fs.existsSync, - readFileSync: (filename: string) => string = (filename: string) => - fs.readFileSync(filename, "utf8") -): Tsconfig | undefined { - if (!existsSync(configFilePath)) { - return undefined; - } - - const configString = readFileSync(configFilePath); - const cleanedJson = StripBom(configString); - let config: Tsconfig; - try { - config = JSON5.parse(cleanedJson); - } catch (e) { - throw new Error(`${configFilePath} is malformed ${e.message}`); - } - let extendedConfig = config.extends; - - if (extendedConfig) { - if ( - typeof extendedConfig === "string" && - extendedConfig.indexOf(".json") === -1 - ) { - extendedConfig += ".json"; - } - const currentDir = path.dirname(configFilePath); - let extendedConfigPath = path.join(currentDir, extendedConfig); - if ( - extendedConfig.indexOf("/") !== -1 && - extendedConfig.indexOf(".") !== -1 && - !existsSync(extendedConfigPath) - ) { - extendedConfigPath = path.join( - currentDir, - "node_modules", - extendedConfig - ); - } - - const base = - loadTsconfig(extendedConfigPath, existsSync, readFileSync) || {}; - - // baseUrl should be interpreted as relative to the base tsconfig, - // but we need to update it so it is relative to the original tsconfig being loaded - if (base.compilerOptions && base.compilerOptions.baseUrl) { - const extendsDir = path.dirname(extendedConfig); - base.compilerOptions.baseUrl = path.join( - extendsDir, - base.compilerOptions.baseUrl - ); - } - - return { - ...base, - ...config, - compilerOptions: { - ...base.compilerOptions, - ...config.compilerOptions, - }, - }; - } - return config; -}