Skip to content

Commit

Permalink
TS 5.0 Supporting Multiple Configuration Files in extends (#1958)
Browse files Browse the repository at this point in the history
* extract tsconfig tests to new spec file; add failing test for "extends" from multiple configs

* Add support for `"extends": []` extending from multiple configs

* Fix tests

* fix test

* fix test on windows

* fmt

* fix
  • Loading branch information
cspotcode authored Feb 13, 2023
1 parent 1c5857c commit ece352f
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 150 deletions.
90 changes: 54 additions & 36 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function readConfig(
}> = [];
let config: any = { compilerOptions: {} };
let basePath = cwd;
let configFilePath: string | undefined = undefined;
let rootConfigPath: string | undefined = undefined;
const projectSearchDir = resolve(cwd, rawApiOptions.projectSearchDir ?? cwd);

const {
Expand All @@ -170,62 +170,80 @@ export function readConfig(
if (project) {
const resolved = resolve(cwd, project);
const nested = join(resolved, 'tsconfig.json');
configFilePath = fileExists(nested) ? nested : resolved;
rootConfigPath = fileExists(nested) ? nested : resolved;
} else {
configFilePath = ts.findConfigFile(projectSearchDir, fileExists);
rootConfigPath = ts.findConfigFile(projectSearchDir, fileExists);
}

if (configFilePath) {
let pathToNextConfigInChain = configFilePath;
if (rootConfigPath) {
// If root extends [a, c] and a extends b, c extends d, then this array will look like:
// [root, c, d, a, b]
let configPaths = [rootConfigPath];
const tsInternals = createTsInternals(ts);
const errors: Array<_ts.Diagnostic> = [];

// Follow chain of "extends"
while (true) {
const result = ts.readConfigFile(pathToNextConfigInChain, readFile);
for (
let configPathIndex = 0;
configPathIndex < configPaths.length;
configPathIndex++
) {
const configPath = configPaths[configPathIndex];
const result = ts.readConfigFile(configPath, readFile);

// Return diagnostics.
if (result.error) {
return {
configFilePath,
configFilePath: rootConfigPath,
config: { errors: [result.error], fileNames: [], options: {} },
tsNodeOptionsFromTsconfig: {},
optionBasePaths: {},
};
}

const c = result.config;
const bp = dirname(pathToNextConfigInChain);
const bp = dirname(configPath);
configChain.push({
config: c,
basePath: bp,
configPath: pathToNextConfigInChain,
configPath: configPath,
});

if (c.extends == null) break;
const resolvedExtendedConfigPath = tsInternals.getExtendsConfigPath(
c.extends,
{
fileExists,
readDirectory: ts.sys.readDirectory,
readFile,
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
trace: tsTrace,
},
bp,
errors,
(ts as unknown as TSInternal).createCompilerDiagnostic
);
if (errors.length) {
return {
configFilePath,
config: { errors, fileNames: [], options: {} },
tsNodeOptionsFromTsconfig: {},
optionBasePaths: {},
};
if (c.extends == null) continue;
const extendsArray = Array.isArray(c.extends) ? c.extends : [c.extends];
for (const e of extendsArray) {
const resolvedExtendedConfigPath = tsInternals.getExtendsConfigPath(
e,
{
fileExists,
readDirectory: ts.sys.readDirectory,
readFile,
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
trace: tsTrace,
},
bp,
errors,
(ts as unknown as TSInternal).createCompilerDiagnostic
);
if (errors.length) {
return {
configFilePath: rootConfigPath,
config: { errors, fileNames: [], options: {} },
tsNodeOptionsFromTsconfig: {},
optionBasePaths: {},
};
}
if (resolvedExtendedConfigPath != null) {
// Tricky! If "extends" array is [a, c] then this will splice them into this order:
// [root, c, a]
// This is what we want.
configPaths.splice(
configPathIndex + 1,
0,
resolvedExtendedConfigPath
);
}
}
if (resolvedExtendedConfigPath == null) break;
pathToNextConfigInChain = resolvedExtendedConfigPath;
}

({ config, basePath } = configChain[0]);
Expand Down Expand Up @@ -277,7 +295,7 @@ export function readConfig(
rawApiOptions.files ?? tsNodeOptionsFromTsconfig.files ?? DEFAULTS.files;

// Only if a config file is *not* loaded, load an implicit configuration from @tsconfig/bases
const skipDefaultCompilerOptions = configFilePath != null;
const skipDefaultCompilerOptions = rootConfigPath != null;
const defaultCompilerOptionsForNodeVersion = skipDefaultCompilerOptions
? undefined
: {
Expand Down Expand Up @@ -316,12 +334,12 @@ export function readConfig(
},
basePath,
undefined,
configFilePath
rootConfigPath
)
);

return {
configFilePath,
configFilePath: rootConfigPath,
config: fixedConfig,
tsNodeOptionsFromTsconfig,
optionBasePaths,
Expand Down
181 changes: 181 additions & 0 deletions src/test/configuration/tsnode-opts-from-tsconfig.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { BIN_PATH } from '../helpers/paths';
import { createExec } from '../exec-helpers';
import { TEST_DIR } from '../helpers/paths';
import { context, expect } from '../testlib';
import { join, resolve } from 'path';
import { tsSupportsExtendsArray } from '../helpers/version-checks';
import { ctxTsNode } from '../helpers';

const test = context(ctxTsNode);

const exec = createExec({
cwd: TEST_DIR,
});

test.suite('should read ts-node options from tsconfig.json', (test) => {
const BIN_EXEC = `"${BIN_PATH}" --project tsconfig-options/tsconfig.json`;

test('should override compiler options from env', async () => {
const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`, {
env: {
...process.env,
TS_NODE_COMPILER_OPTIONS: '{"typeRoots": ["env-typeroots"]}',
},
});
expect(r.err).toBe(null);
const { config } = JSON.parse(r.stdout);
expect(config.options.typeRoots).toEqual([
join(TEST_DIR, './tsconfig-options/env-typeroots').replace(/\\/g, '/'),
]);
});

test('should use options from `tsconfig.json`', async () => {
const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`);
expect(r.err).toBe(null);
const { options, config } = JSON.parse(r.stdout);
expect(config.options.typeRoots).toEqual([
join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace(
/\\/g,
'/'
),
]);
expect(config.options.types).toEqual(['tsconfig-tsnode-types']);
expect(options.pretty).toBe(undefined);
expect(options.skipIgnore).toBe(false);
expect(options.transpileOnly).toBe(true);
expect(options.require).toEqual([
join(TEST_DIR, './tsconfig-options/required1.js'),
]);
});

test('should ignore empty strings in the array options', async () => {
const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`, {
env: {
...process.env,
TS_NODE_IGNORE: '',
},
});
expect(r.err).toBe(null);
const { options } = JSON.parse(r.stdout);
expect(options.ignore).toEqual([]);
});

test('should have flags override / merge with `tsconfig.json`', async () => {
const r = await exec(
`${BIN_EXEC} --skip-ignore --compiler-options "{\\"types\\":[\\"flags-types\\"]}" --require ./tsconfig-options/required2.js tsconfig-options/log-options2.js`
);
expect(r.err).toBe(null);
const { options, config } = JSON.parse(r.stdout);
expect(config.options.typeRoots).toEqual([
join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace(
/\\/g,
'/'
),
]);
expect(config.options.types).toEqual(['flags-types']);
expect(options.pretty).toBe(undefined);
expect(options.skipIgnore).toBe(true);
expect(options.transpileOnly).toBe(true);
expect(options.require).toEqual([
join(TEST_DIR, './tsconfig-options/required1.js'),
'./tsconfig-options/required2.js',
]);
});

test('should have `tsconfig.json` override environment', async () => {
const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`, {
env: {
...process.env,
TS_NODE_PRETTY: 'true',
TS_NODE_SKIP_IGNORE: 'true',
},
});
expect(r.err).toBe(null);
const { options, config } = JSON.parse(r.stdout);
expect(config.options.typeRoots).toEqual([
join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace(
/\\/g,
'/'
),
]);
expect(config.options.types).toEqual(['tsconfig-tsnode-types']);
expect(options.pretty).toBe(true);
expect(options.skipIgnore).toBe(false);
expect(options.transpileOnly).toBe(true);
expect(options.require).toEqual([
join(TEST_DIR, './tsconfig-options/required1.js'),
]);
});

test('should pull ts-node options from extended `tsconfig.json`', async () => {
const r = await exec(
`${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json`
);
expect(r.err).toBe(null);
const config = JSON.parse(r.stdout);
expect(config['ts-node'].require).toEqual([
resolve(TEST_DIR, 'tsconfig-extends/other/require-hook.js'),
]);
expect(config['ts-node'].scopeDir).toBe(
resolve(TEST_DIR, 'tsconfig-extends/other/scopedir')
);
expect(config['ts-node'].preferTsExts).toBe(true);
});

test.suite(
'should pull ts-node options from extended `tsconfig.json`',
(test) => {
test.if(tsSupportsExtendsArray);
test('test', async () => {
const r = await exec(
`${BIN_PATH} --show-config --project ./tsconfig-extends-multiple/tsconfig.json`
);
expect(r.err).toBe(null);
const config = JSON.parse(r.stdout);

// root tsconfig extends [a, c]
// a extends b
// c extends d

// https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#supporting-multiple-configuration-files-in-extends
// If any fields "conflict", the latter entry wins.

// This value comes from c
expect(config.compilerOptions.target).toBe('es2017');

// From root
expect(config['ts-node'].preferTsExts).toBe(true);

// From a
expect(config['ts-node'].require).toEqual([
resolve(
TEST_DIR,
'tsconfig-extends-multiple/a/require-hook-from-a.js'
),
]);

// From a, overrides declaration in b
expect(config['ts-node'].scopeDir).toBe(
resolve(TEST_DIR, 'tsconfig-extends-multiple/a/scopedir-from-a')
);

// From b
const key =
process.platform === 'win32'
? 'b\\module-types-from-b'
: 'b/module-types-from-b';
expect(config['ts-node'].moduleTypes).toStrictEqual({
[key]: 'cjs',
});

// From c, overrides declaration in b
expect(config['ts-node'].transpiler).toBe('transpiler-from-c');

// From d, inherited by c, overrides value from b
expect(config['ts-node'].ignore).toStrictEqual([
'ignore-pattern-from-d',
]);
});
}
);
});
2 changes: 2 additions & 0 deletions src/test/helpers/version-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const tsSupportsAllowImportingTsExtensions = semver.gte(
ts.version,
'4.999.999'
);
// TS 5.0 adds ability for tsconfig to `"extends": []` an array of configs
export const tsSupportsExtendsArray = semver.gte(ts.version, '4.999.999');
// Relevant when @tsconfig/bases refers to es2021 and we run tests against
// old TS versions.
export const tsSupportsEs2021 = semver.gte(ts.version, '4.3.0');
Expand Down
Loading

0 comments on commit ece352f

Please sign in to comment.