Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use typescript library to resolve tsconfig files #246

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions example/inherited/tsconfig.base.json
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@": []
}
}
}
3 changes: 3 additions & 0 deletions example/inherited/tsconfig.json
@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.preset.json"
}
3 changes: 3 additions & 0 deletions example/inherited/tsconfig.preset.json
@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.base.json"
}
7 changes: 4 additions & 3 deletions package.json
Expand Up @@ -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",
Expand Down
178 changes: 12 additions & 166 deletions 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", () => {
Expand Down Expand Up @@ -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"),
});
});
});
105 changes: 16 additions & 89 deletions 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<string> };
strict?: boolean;
};
}
import * as typescript from "typescript";

export interface TsConfigLoaderResult {
tsConfigPath: string | undefined;
Expand Down Expand Up @@ -47,7 +32,7 @@ export function tsConfigLoader({
return loadResult;
}

function loadSyncDefault(
export function loadSyncDefault(
cwd: string,
filename?: string,
baseUrl?: string
Expand All @@ -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,
};
}

Expand Down Expand Up @@ -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;
}