@@ -3,6 +3,8 @@ import path from 'node:path';
3
3
import _test , { type TestFn } from 'ava' ; // eslint-disable-line ava/use-test
4
4
import dedent from 'dedent' ;
5
5
import { $ , type ExecaError } from 'execa' ;
6
+ import { pathExists } from 'path-exists' ;
7
+ import { type TsConfigJson } from 'get-tsconfig' ;
6
8
import { copyTestProject } from './helpers/copy-test-project.js' ;
7
9
8
10
const test = _test as TestFn < { cwd : string } > ;
@@ -352,3 +354,116 @@ test('Config errors bubble up from ESLint when incorrect config options are set'
352
354
const error = await t . throwsAsync < ExecaError > ( $ `node ./dist/cli --cwd ${ t . context . cwd } ` ) ;
353
355
t . true ( ( error . stderr as string ) ?. includes ( 'ConfigError:' ) && ( error . stderr as string ) ?. includes ( 'Unexpected key "invalidOption" found' ) ) ;
354
356
} ) ;
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