| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import * as path from "path";
- import * as fs from "fs";
- // tslint:disable:no-require-imports
- import JSON5 = require("json5");
- import StripBom = require("strip-bom");
- // tslint:enable:no-require-imports
- /**
- * Typing for the parts of tsconfig that we care about
- */
- export interface Tsconfig {
- extends?: string | string[];
- compilerOptions?: {
- baseUrl?: string;
- paths?: { [key: string]: Array<string> };
- strict?: boolean;
- };
- }
- export interface TsConfigLoaderResult {
- tsConfigPath: string | undefined;
- baseUrl: string | undefined;
- paths: { [key: string]: Array<string> } | undefined;
- }
- export interface TsConfigLoaderParams {
- getEnv: (key: string) => string | undefined;
- cwd: string;
- loadSync?(
- cwd: string,
- filename?: string,
- baseUrl?: string
- ): TsConfigLoaderResult;
- }
- export function tsConfigLoader({
- getEnv,
- cwd,
- loadSync = loadSyncDefault,
- }: TsConfigLoaderParams): TsConfigLoaderResult {
- const TS_NODE_PROJECT = getEnv("TS_NODE_PROJECT");
- const TS_NODE_BASEURL = getEnv("TS_NODE_BASEURL");
- // tsconfig.loadSync handles if TS_NODE_PROJECT is a file or directory
- // and also overrides baseURL if TS_NODE_BASEURL is available.
- const loadResult = loadSync(cwd, TS_NODE_PROJECT, TS_NODE_BASEURL);
- return loadResult;
- }
- function loadSyncDefault(
- cwd: string,
- filename?: string,
- baseUrl?: string
- ): TsConfigLoaderResult {
- // Tsconfig.loadSync uses path.resolve. This is why we can use an absolute path as filename
- const configPath = resolveConfigPath(cwd, filename);
- if (!configPath) {
- return {
- tsConfigPath: undefined,
- baseUrl: undefined,
- paths: undefined,
- };
- }
- const config = loadTsconfig(configPath);
- return {
- tsConfigPath: configPath,
- baseUrl:
- baseUrl ||
- (config && config.compilerOptions && config.compilerOptions.baseUrl),
- paths: config && config.compilerOptions && config.compilerOptions.paths,
- };
- }
- function resolveConfigPath(cwd: string, filename?: string): string | undefined {
- if (filename) {
- const absolutePath = fs.lstatSync(filename).isDirectory()
- ? path.resolve(filename, "./tsconfig.json")
- : path.resolve(cwd, filename);
- return absolutePath;
- }
- if (fs.statSync(cwd).isFile()) {
- return path.resolve(cwd);
- }
- const configAbsolutePath = walkForTsConfig(cwd);
- return configAbsolutePath ? path.resolve(configAbsolutePath) : undefined;
- }
- export function walkForTsConfig(
- directory: string,
- existsSync: (path: string) => boolean = fs.existsSync
- ): string | undefined {
- const configPath = path.join(directory, "./tsconfig.json");
- if (existsSync(configPath)) {
- return configPath;
- }
- const parentDirectory = path.join(directory, "../");
- // If we reached the top
- if (directory === parentDirectory) {
- return undefined;
- }
- return walkForTsConfig(parentDirectory, existsSync);
- }
- export function loadTsconfig(
- configFilePath: string,
- 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) {
- let base: Tsconfig;
- if (Array.isArray(extendedConfig)) {
- base = extendedConfig.reduce(
- (currBase, extendedConfigElement) =>
- mergeTsconfigs(
- currBase,
- loadTsconfigFromExtends(
- configFilePath,
- extendedConfigElement,
- existsSync,
- readFileSync
- )
- ),
- {}
- );
- } else {
- base = loadTsconfigFromExtends(
- configFilePath,
- extendedConfig,
- existsSync,
- readFileSync
- );
- }
- return mergeTsconfigs(base, config);
- }
- return config;
- }
- /**
- * Intended to be called only from loadTsconfig.
- * Parameters don't have defaults because they should use the same as loadTsconfig.
- */
- function loadTsconfigFromExtends(
- configFilePath: string,
- extendedConfigValue: string,
- // eslint-disable-next-line no-shadow
- existsSync: (path: string) => boolean,
- readFileSync: (filename: string) => string
- ): Tsconfig {
- if (
- typeof extendedConfigValue === "string" &&
- extendedConfigValue.indexOf(".json") === -1
- ) {
- extendedConfigValue += ".json";
- }
- const currentDir = path.dirname(configFilePath);
- let extendedConfigPath = path.join(currentDir, extendedConfigValue);
- if (
- extendedConfigValue.indexOf("/") !== -1 &&
- extendedConfigValue.indexOf(".") !== -1 &&
- !existsSync(extendedConfigPath)
- ) {
- extendedConfigPath = path.join(
- currentDir,
- "node_modules",
- extendedConfigValue
- );
- }
- const config =
- loadTsconfig(extendedConfigPath, existsSync, readFileSync) || {};
- // baseUrl should be interpreted as relative to extendedConfigPath,
- // but we need to update it so it is relative to the original tsconfig being loaded
- if (config.compilerOptions?.baseUrl) {
- const extendsDir = path.dirname(extendedConfigValue);
- config.compilerOptions.baseUrl = path.join(
- extendsDir,
- config.compilerOptions.baseUrl
- );
- }
- return config;
- }
- function mergeTsconfigs(
- base: Tsconfig | undefined,
- config: Tsconfig | undefined
- ): Tsconfig {
- base = base || {};
- config = config || {};
- return {
- ...base,
- ...config,
- compilerOptions: {
- ...base.compilerOptions,
- ...config.compilerOptions,
- },
- };
- }
|