Skip to content

Commit e280d32

Browse files
authored
Improve tsconfig handling (#810)
1 parent 412e3ae commit e280d32

File tree

2 files changed

+124
-38
lines changed

2 files changed

+124
-38
lines changed

lib/handle-ts-files.ts

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import path from 'node:path';
22
import fs from 'node:fs/promises';
3-
import {getTsconfig} from 'get-tsconfig';
4-
import micromatch, {type Options} from 'micromatch';
5-
import {tsconfigDefaults, cacheDirName} from './constants.js';
6-
7-
const micromatchOptions: Options = {matchBase: true};
3+
import {getTsconfig, createFilesMatcher} from 'get-tsconfig';
4+
import {tsconfigDefaults as tsConfig, cacheDirName} from './constants.js';
85

96
/**
107
This function checks if the files are matched by the tsconfig include, exclude, and it returns the unmatched files.
@@ -15,49 +12,23 @@ If no tsconfig is found, it will create a fallback tsconfig file in the `node_mo
1512
@returns The unmatched files.
1613
*/
1714
export async function handleTsconfig({cwd, files}: {cwd: string; files: string[]}) {
18-
const {config: tsConfig = tsconfigDefaults, path: tsConfigPath} = getTsconfig(cwd) ?? {};
19-
20-
tsConfig.compilerOptions ??= {};
21-
2215
const unincludedFiles: string[] = [];
2316

2417
for (const filePath of files) {
25-
let hasMatch = false;
18+
const result = getTsconfig(filePath);
2619

27-
if (!tsConfigPath) {
20+
if (!result) {
2821
unincludedFiles.push(filePath);
2922
continue;
3023
}
3124

32-
// If there is no files or include property - TS uses `**/*` as default so all TS files are matched.
33-
// In tsconfig, excludes override includes - so we need to prioritize that matching logic.
34-
if (
35-
tsConfig
36-
&& !tsConfig.include
37-
&& !tsConfig.files
38-
) {
39-
// If we have an excludes property, we need to check it.
40-
// If we match on excluded, then we definitively know that there is no tsconfig match.
41-
if (Array.isArray(tsConfig.exclude)) {
42-
const exclude = Array.isArray(tsConfig.exclude) ? tsConfig.exclude : [];
43-
hasMatch = !micromatch.contains(filePath, exclude, micromatchOptions);
44-
} else {
45-
// Not explicitly excluded and included by tsconfig defaults
46-
hasMatch = true;
47-
}
48-
} else {
49-
// We have either and include or a files property in tsconfig
50-
const include = Array.isArray(tsConfig.include) ? tsConfig.include : [];
51-
const files = Array.isArray(tsConfig.files) ? tsConfig.files : [];
52-
const exclude = Array.isArray(tsConfig.exclude) ? tsConfig.exclude : [];
53-
// If we also have an exlcude we need to check all the arrays, (files, include, exclude)
54-
// this check not excluded and included in one of the file/include array
55-
hasMatch = !micromatch.contains(filePath, exclude, micromatchOptions) && micromatch.isMatch(filePath, [...include, ...files], micromatchOptions);
56-
}
25+
const filesMatcher = createFilesMatcher(result);
5726

58-
if (!hasMatch) {
59-
unincludedFiles.push(filePath);
27+
if (filesMatcher(filePath)) {
28+
continue;
6029
}
30+
31+
unincludedFiles.push(filePath);
6132
}
6233

6334
const fallbackTsConfigPath = path.join(cwd, 'node_modules', '.cache', cacheDirName, 'tsconfig.xo.json');

test/cli.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import path from 'node:path';
33
import _test, {type TestFn} from 'ava'; // eslint-disable-line ava/use-test
44
import dedent from 'dedent';
55
import {$, type ExecaError} from 'execa';
6+
import {pathExists} from 'path-exists';
7+
import {type TsConfigJson} from 'get-tsconfig';
68
import {copyTestProject} from './helpers/copy-test-project.js';
79

810
const test = _test as TestFn<{cwd: string}>;
@@ -352,3 +354,116 @@ test('Config errors bubble up from ESLint when incorrect config options are set'
352354
const error = await t.throwsAsync<ExecaError>($`node ./dist/cli --cwd ${t.context.cwd}`);
353355
t.true((error.stderr as string)?.includes('ConfigError:') && (error.stderr as string)?.includes('Unexpected key "invalidOption" found'));
354356
});
357+
358+
test('ts in nested directory', async t => {
359+
const filePath = path.join(t.context.cwd, 'nested', 'src', 'test.ts');
360+
const baseTsConfigPath = path.join(t.context.cwd, 'tsconfig.json');
361+
const tsConfigNestedPath = path.join(t.context.cwd, 'nested', 'tsconfig.json');
362+
const tsconfigCachePath = path.join(t.context.cwd, 'node_modules', '.cache', 'xo-linter', 'tsconfig.xo.json');
363+
364+
// Remove any previous cache file
365+
await fs.rm(tsconfigCachePath, {force: true});
366+
367+
// Write the test.ts file
368+
await fs.mkdir(path.dirname(filePath), {recursive: true});
369+
await fs.writeFile(filePath, dedent`console.log('hello');\nconst test = 1;\n`, 'utf8');
370+
371+
// Copy the base tsconfig to the nested directory
372+
await fs.copyFile(baseTsConfigPath, tsConfigNestedPath);
373+
await fs.rm(baseTsConfigPath);
374+
const tsconfig = JSON.parse(await fs.readFile(tsConfigNestedPath, 'utf8')) as TsConfigJson;
375+
if (tsconfig.compilerOptions) {
376+
tsconfig.compilerOptions.baseUrl = './';
377+
}
378+
379+
tsconfig.include = ['src'];
380+
381+
await fs.writeFile(tsConfigNestedPath, JSON.stringify(tsconfig, null, 2), 'utf8');
382+
// Add an xo config file in root dir
383+
const xoConfigPath = path.join(t.context.cwd, 'xo.config.js');
384+
const xoConfig = dedent`
385+
export default [
386+
{ ignores: "xo.config.js" },
387+
{
388+
rules: {
389+
'@typescript-eslint/no-unused-vars': 'off',
390+
}
391+
}
392+
]
393+
`;
394+
await fs.writeFile(xoConfigPath, xoConfig, 'utf8');
395+
await t.notThrowsAsync($`node ./dist/cli --cwd ${t.context.cwd}`);
396+
t.false(await pathExists(tsconfigCachePath), 'tsconfig.xo.json should not be created in the cache directory when tsconfig.json is present in the nested directory');
397+
});
398+
399+
test('handles mixed project structure with nested tsconfig and root ts files', async t => {
400+
// Set up nested TypeScript files with a tsconfig
401+
const nestedFilePath = path.join(t.context.cwd, 'nested', 'src', 'test.ts');
402+
const nestedFile2Path = path.join(t.context.cwd, 'nested', 'src', 'test2.ts');
403+
const baseTsConfigPath = path.join(t.context.cwd, 'tsconfig.json');
404+
const tsConfigNestedPath = path.join(t.context.cwd, 'nested', 'tsconfig.json');
405+
const tsconfigCachePath = path.join(t.context.cwd, 'node_modules', '.cache', 'xo-linter', 'tsconfig.xo.json');
406+
407+
// Root ts file with no tsconfig
408+
const rootTsFilePath = path.join(t.context.cwd, 'root.ts');
409+
410+
// Remove any previous cache file
411+
await fs.rm(tsconfigCachePath, {force: true});
412+
413+
// Create directory structure and files
414+
await fs.mkdir(path.dirname(nestedFilePath), {recursive: true});
415+
await fs.writeFile(nestedFilePath, dedent`console.log('nested file 1');\nconst test1 = 1;\n`, 'utf8');
416+
await fs.writeFile(nestedFile2Path, dedent`console.log('nested file 2');\nconst test2 = 2;\n`, 'utf8');
417+
418+
// Create the root TS file with no accompanying tsconfig
419+
await fs.writeFile(rootTsFilePath, dedent`console.log('root file');\nconst rootVar = 3;\n`, 'utf8');
420+
421+
// Copy the base tsconfig to the nested directory only
422+
await fs.copyFile(baseTsConfigPath, tsConfigNestedPath);
423+
await fs.rm(baseTsConfigPath);
424+
const tsconfig = JSON.parse(await fs.readFile(tsConfigNestedPath, 'utf8')) as TsConfigJson;
425+
426+
if (tsconfig.compilerOptions) {
427+
tsconfig.compilerOptions.baseUrl = './';
428+
}
429+
430+
// Configure the nested tsconfig to include only the nested src directory
431+
tsconfig.include = ['src'];
432+
await fs.writeFile(tsConfigNestedPath, JSON.stringify(tsconfig, null, 2), 'utf8');
433+
434+
// Add an xo config file in root dir
435+
const xoConfigPath = path.join(t.context.cwd, 'xo.config.js');
436+
const xoConfig = dedent`
437+
export default [
438+
{ ignores: "xo.config.js" },
439+
{
440+
rules: {
441+
'@typescript-eslint/no-unused-vars': 'off',
442+
}
443+
}
444+
]
445+
`;
446+
await fs.writeFile(xoConfigPath, xoConfig, 'utf8');
447+
448+
// Run XO on the entire directory structure
449+
await t.notThrowsAsync($`node ./dist/cli --cwd ${t.context.cwd}`);
450+
451+
// Verify the cache file was created
452+
t.true(await pathExists(tsconfigCachePath), 'tsconfig.xo.json should be created for files not covered by existing tsconfigs');
453+
454+
// Check the content of the cached tsconfig
455+
const cachedTsConfig = JSON.parse(await fs.readFile(tsconfigCachePath, 'utf8')) as TsConfigJson;
456+
457+
// Verify only the root.ts file is in the cached tsconfig (not the nested files)
458+
t.deepEqual(cachedTsConfig.files, [rootTsFilePath], 'tsconfig.xo.json should only contain the root.ts file not covered by existing tsconfig');
459+
460+
// Verify the nested files aren't included (they should be covered by the nested tsconfig)
461+
t.false(
462+
cachedTsConfig.files?.includes(nestedFilePath),
463+
'tsconfig.xo.json should not include files already covered by nested tsconfig',
464+
);
465+
t.false(
466+
cachedTsConfig.files?.includes(nestedFile2Path),
467+
'tsconfig.xo.json should not include files already covered by nested tsconfig',
468+
);
469+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy