| [9af201e] | 1 | import * as path from "path";
|
|---|
| 2 | import * as fs from "fs";
|
|---|
| 3 | // tslint:disable:no-require-imports
|
|---|
| 4 | import JSON5 = require("json5");
|
|---|
| 5 | import StripBom = require("strip-bom");
|
|---|
| 6 | // tslint:enable:no-require-imports
|
|---|
| 7 |
|
|---|
| 8 | /**
|
|---|
| 9 | * Typing for the parts of tsconfig that we care about
|
|---|
| 10 | */
|
|---|
| 11 | export interface Tsconfig {
|
|---|
| 12 | extends?: string | string[];
|
|---|
| 13 | compilerOptions?: {
|
|---|
| 14 | baseUrl?: string;
|
|---|
| 15 | paths?: { [key: string]: Array<string> };
|
|---|
| 16 | strict?: boolean;
|
|---|
| 17 | };
|
|---|
| 18 | }
|
|---|
| 19 |
|
|---|
| 20 | export interface TsConfigLoaderResult {
|
|---|
| 21 | tsConfigPath: string | undefined;
|
|---|
| 22 | baseUrl: string | undefined;
|
|---|
| 23 | paths: { [key: string]: Array<string> } | undefined;
|
|---|
| 24 | }
|
|---|
| 25 |
|
|---|
| 26 | export interface TsConfigLoaderParams {
|
|---|
| 27 | getEnv: (key: string) => string | undefined;
|
|---|
| 28 | cwd: string;
|
|---|
| 29 | loadSync?(
|
|---|
| 30 | cwd: string,
|
|---|
| 31 | filename?: string,
|
|---|
| 32 | baseUrl?: string
|
|---|
| 33 | ): TsConfigLoaderResult;
|
|---|
| 34 | }
|
|---|
| 35 |
|
|---|
| 36 | export function tsConfigLoader({
|
|---|
| 37 | getEnv,
|
|---|
| 38 | cwd,
|
|---|
| 39 | loadSync = loadSyncDefault,
|
|---|
| 40 | }: TsConfigLoaderParams): TsConfigLoaderResult {
|
|---|
| 41 | const TS_NODE_PROJECT = getEnv("TS_NODE_PROJECT");
|
|---|
| 42 | const TS_NODE_BASEURL = getEnv("TS_NODE_BASEURL");
|
|---|
| 43 |
|
|---|
| 44 | // tsconfig.loadSync handles if TS_NODE_PROJECT is a file or directory
|
|---|
| 45 | // and also overrides baseURL if TS_NODE_BASEURL is available.
|
|---|
| 46 | const loadResult = loadSync(cwd, TS_NODE_PROJECT, TS_NODE_BASEURL);
|
|---|
| 47 | return loadResult;
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | function loadSyncDefault(
|
|---|
| 51 | cwd: string,
|
|---|
| 52 | filename?: string,
|
|---|
| 53 | baseUrl?: string
|
|---|
| 54 | ): TsConfigLoaderResult {
|
|---|
| 55 | // Tsconfig.loadSync uses path.resolve. This is why we can use an absolute path as filename
|
|---|
| 56 |
|
|---|
| 57 | const configPath = resolveConfigPath(cwd, filename);
|
|---|
| 58 |
|
|---|
| 59 | if (!configPath) {
|
|---|
| 60 | return {
|
|---|
| 61 | tsConfigPath: undefined,
|
|---|
| 62 | baseUrl: undefined,
|
|---|
| 63 | paths: undefined,
|
|---|
| 64 | };
|
|---|
| 65 | }
|
|---|
| 66 | const config = loadTsconfig(configPath);
|
|---|
| 67 |
|
|---|
| 68 | return {
|
|---|
| 69 | tsConfigPath: configPath,
|
|---|
| 70 | baseUrl:
|
|---|
| 71 | baseUrl ||
|
|---|
| 72 | (config && config.compilerOptions && config.compilerOptions.baseUrl),
|
|---|
| 73 | paths: config && config.compilerOptions && config.compilerOptions.paths,
|
|---|
| 74 | };
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | function resolveConfigPath(cwd: string, filename?: string): string | undefined {
|
|---|
| 78 | if (filename) {
|
|---|
| 79 | const absolutePath = fs.lstatSync(filename).isDirectory()
|
|---|
| 80 | ? path.resolve(filename, "./tsconfig.json")
|
|---|
| 81 | : path.resolve(cwd, filename);
|
|---|
| 82 |
|
|---|
| 83 | return absolutePath;
|
|---|
| 84 | }
|
|---|
| 85 |
|
|---|
| 86 | if (fs.statSync(cwd).isFile()) {
|
|---|
| 87 | return path.resolve(cwd);
|
|---|
| 88 | }
|
|---|
| 89 |
|
|---|
| 90 | const configAbsolutePath = walkForTsConfig(cwd);
|
|---|
| 91 | return configAbsolutePath ? path.resolve(configAbsolutePath) : undefined;
|
|---|
| 92 | }
|
|---|
| 93 |
|
|---|
| 94 | export function walkForTsConfig(
|
|---|
| 95 | directory: string,
|
|---|
| 96 | existsSync: (path: string) => boolean = fs.existsSync
|
|---|
| 97 | ): string | undefined {
|
|---|
| 98 | const configPath = path.join(directory, "./tsconfig.json");
|
|---|
| 99 | if (existsSync(configPath)) {
|
|---|
| 100 | return configPath;
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | const parentDirectory = path.join(directory, "../");
|
|---|
| 104 |
|
|---|
| 105 | // If we reached the top
|
|---|
| 106 | if (directory === parentDirectory) {
|
|---|
| 107 | return undefined;
|
|---|
| 108 | }
|
|---|
| 109 |
|
|---|
| 110 | return walkForTsConfig(parentDirectory, existsSync);
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | export function loadTsconfig(
|
|---|
| 114 | configFilePath: string,
|
|---|
| 115 | existsSync: (path: string) => boolean = fs.existsSync,
|
|---|
| 116 | readFileSync: (filename: string) => string = (filename: string) =>
|
|---|
| 117 | fs.readFileSync(filename, "utf8")
|
|---|
| 118 | ): Tsconfig | undefined {
|
|---|
| 119 | if (!existsSync(configFilePath)) {
|
|---|
| 120 | return undefined;
|
|---|
| 121 | }
|
|---|
| 122 |
|
|---|
| 123 | const configString = readFileSync(configFilePath);
|
|---|
| 124 | const cleanedJson = StripBom(configString);
|
|---|
| 125 | let config: Tsconfig;
|
|---|
| 126 | try {
|
|---|
| 127 | config = JSON5.parse(cleanedJson);
|
|---|
| 128 | } catch (e) {
|
|---|
| 129 | throw new Error(`${configFilePath} is malformed ${e.message}`);
|
|---|
| 130 | }
|
|---|
| 131 |
|
|---|
| 132 | let extendedConfig = config.extends;
|
|---|
| 133 | if (extendedConfig) {
|
|---|
| 134 | let base: Tsconfig;
|
|---|
| 135 |
|
|---|
| 136 | if (Array.isArray(extendedConfig)) {
|
|---|
| 137 | base = extendedConfig.reduce(
|
|---|
| 138 | (currBase, extendedConfigElement) =>
|
|---|
| 139 | mergeTsconfigs(
|
|---|
| 140 | currBase,
|
|---|
| 141 | loadTsconfigFromExtends(
|
|---|
| 142 | configFilePath,
|
|---|
| 143 | extendedConfigElement,
|
|---|
| 144 | existsSync,
|
|---|
| 145 | readFileSync
|
|---|
| 146 | )
|
|---|
| 147 | ),
|
|---|
| 148 | {}
|
|---|
| 149 | );
|
|---|
| 150 | } else {
|
|---|
| 151 | base = loadTsconfigFromExtends(
|
|---|
| 152 | configFilePath,
|
|---|
| 153 | extendedConfig,
|
|---|
| 154 | existsSync,
|
|---|
| 155 | readFileSync
|
|---|
| 156 | );
|
|---|
| 157 | }
|
|---|
| 158 |
|
|---|
| 159 | return mergeTsconfigs(base, config);
|
|---|
| 160 | }
|
|---|
| 161 | return config;
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | /**
|
|---|
| 165 | * Intended to be called only from loadTsconfig.
|
|---|
| 166 | * Parameters don't have defaults because they should use the same as loadTsconfig.
|
|---|
| 167 | */
|
|---|
| 168 | function loadTsconfigFromExtends(
|
|---|
| 169 | configFilePath: string,
|
|---|
| 170 | extendedConfigValue: string,
|
|---|
| 171 | // eslint-disable-next-line no-shadow
|
|---|
| 172 | existsSync: (path: string) => boolean,
|
|---|
| 173 | readFileSync: (filename: string) => string
|
|---|
| 174 | ): Tsconfig {
|
|---|
| 175 | if (
|
|---|
| 176 | typeof extendedConfigValue === "string" &&
|
|---|
| 177 | extendedConfigValue.indexOf(".json") === -1
|
|---|
| 178 | ) {
|
|---|
| 179 | extendedConfigValue += ".json";
|
|---|
| 180 | }
|
|---|
| 181 | const currentDir = path.dirname(configFilePath);
|
|---|
| 182 | let extendedConfigPath = path.join(currentDir, extendedConfigValue);
|
|---|
| 183 | if (
|
|---|
| 184 | extendedConfigValue.indexOf("/") !== -1 &&
|
|---|
| 185 | extendedConfigValue.indexOf(".") !== -1 &&
|
|---|
| 186 | !existsSync(extendedConfigPath)
|
|---|
| 187 | ) {
|
|---|
| 188 | extendedConfigPath = path.join(
|
|---|
| 189 | currentDir,
|
|---|
| 190 | "node_modules",
|
|---|
| 191 | extendedConfigValue
|
|---|
| 192 | );
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | const config =
|
|---|
| 196 | loadTsconfig(extendedConfigPath, existsSync, readFileSync) || {};
|
|---|
| 197 |
|
|---|
| 198 | // baseUrl should be interpreted as relative to extendedConfigPath,
|
|---|
| 199 | // but we need to update it so it is relative to the original tsconfig being loaded
|
|---|
| 200 | if (config.compilerOptions?.baseUrl) {
|
|---|
| 201 | const extendsDir = path.dirname(extendedConfigValue);
|
|---|
| 202 | config.compilerOptions.baseUrl = path.join(
|
|---|
| 203 | extendsDir,
|
|---|
| 204 | config.compilerOptions.baseUrl
|
|---|
| 205 | );
|
|---|
| 206 | }
|
|---|
| 207 |
|
|---|
| 208 | return config;
|
|---|
| 209 | }
|
|---|
| 210 |
|
|---|
| 211 | function mergeTsconfigs(
|
|---|
| 212 | base: Tsconfig | undefined,
|
|---|
| 213 | config: Tsconfig | undefined
|
|---|
| 214 | ): Tsconfig {
|
|---|
| 215 | base = base || {};
|
|---|
| 216 | config = config || {};
|
|---|
| 217 |
|
|---|
| 218 | return {
|
|---|
| 219 | ...base,
|
|---|
| 220 | ...config,
|
|---|
| 221 | compilerOptions: {
|
|---|
| 222 | ...base.compilerOptions,
|
|---|
| 223 | ...config.compilerOptions,
|
|---|
| 224 | },
|
|---|
| 225 | };
|
|---|
| 226 | }
|
|---|