11import path from 'node:path' ;
2- import fs from 'node:fs/promises' ;
2+ import fs from 'node:fs' ;
3+ import ts from 'typescript' ;
34import { getTsconfig , createFilesMatcher } from 'get-tsconfig' ;
4- import { tsconfigDefaults as tsConfig , cacheDirName } from './constants.js' ;
5+ import { tsconfigDefaults } from './constants.js' ;
6+
7+ const createInMemoryProgram = ( files : string [ ] , cwd : string ) : ts . Program | undefined => {
8+ if ( files . length === 0 ) {
9+ return undefined ;
10+ }
11+
12+ try {
13+ const compilerOptions = getFallbackCompilerOptions ( cwd ) ;
14+ const program = ts . createProgram ( files , { ...compilerOptions } ) ;
15+ Object . defineProperty ( program , 'toJSON' , {
16+ value : ( ) => ( {
17+ __type : 'TypeScriptProgram' ,
18+ files : files . map ( file => path . relative ( cwd , file ) ) ,
19+ } ) ,
20+ configurable : true ,
21+ } ) ;
22+
23+ return program ;
24+ } catch ( error ) {
25+ console . warn (
26+ 'XO: Failed to create TypeScript Program for type-aware linting. Continuing without type information for unincluded files.' ,
27+ error instanceof Error ? error . message : String ( error ) ,
28+ ) ;
29+ return undefined ;
30+ }
31+ } ;
32+
33+ const fallbackCompilerOptionsCache = new Map < string , ts . CompilerOptions > ( ) ;
34+
35+ const getFallbackCompilerOptions = ( cwd : string ) : ts . CompilerOptions => {
36+ const cacheKey = path . resolve ( cwd ) ;
37+ const cached = fallbackCompilerOptionsCache . get ( cacheKey ) ;
38+
39+ if ( cached ) {
40+ return cached ;
41+ }
42+
43+ const compilerOptionsResult = ts . convertCompilerOptionsFromJson (
44+ tsconfigDefaults . compilerOptions ?? { } ,
45+ cacheKey ,
46+ ) ;
47+
48+ if ( compilerOptionsResult . errors . length > 0 ) {
49+ throw new Error ( 'XO: Invalid default TypeScript compiler options' ) ;
50+ }
51+
52+ const compilerOptions : ts . CompilerOptions = {
53+ ...compilerOptionsResult . options ,
54+ esModuleInterop : true ,
55+ resolveJsonModules : true ,
56+ allowJs : true ,
57+ skipLibCheck : true ,
58+ skipDefaultLibCheck : true ,
59+ } ;
60+
61+ fallbackCompilerOptionsCache . set ( cacheKey , compilerOptions ) ;
62+ return compilerOptions ;
63+ } ;
564
665/**
766This function checks if the files are matched by the tsconfig include, exclude, and it returns the unmatched files.
867
9- If no tsconfig is found, it will create a fallback tsconfig file in the `node_modules/.cache/xo` directory .
68+ If no tsconfig is found, it will create an in-memory TypeScript Program for type-aware linting .
1069
1170@param options
12- @returns The unmatched files.
71+ @returns The unmatched files and an in-memory TypeScript Program .
1372*/
14- export async function handleTsconfig ( { cwd , files } : { cwd : string ; files : string [ ] } ) {
73+ export function handleTsconfig ( { files , cwd } : { files : string [ ] ; cwd : string } ) {
1574 const unincludedFiles : string [ ] = [ ] ;
75+ const filesMatcherCache = new Map < string , ReturnType < typeof createFilesMatcher > > ( ) ;
1676
1777 for ( const filePath of files ) {
1878 const result = getTsconfig ( filePath ) ;
@@ -22,7 +82,13 @@ export async function handleTsconfig({cwd, files}: {cwd: string; files: string[]
2282 continue ;
2383 }
2484
25- const filesMatcher = createFilesMatcher ( result ) ;
85+ const cacheKey = result . path ? path . resolve ( result . path ) : filePath ;
86+ let filesMatcher = filesMatcherCache . get ( cacheKey ) ;
87+
88+ if ( ! filesMatcher ) {
89+ filesMatcher = createFilesMatcher ( result ) ;
90+ filesMatcherCache . set ( cacheKey , filesMatcher ) ;
91+ }
2692
2793 if ( filesMatcher ( filePath ) ) {
2894 continue ;
@@ -31,17 +97,15 @@ export async function handleTsconfig({cwd, files}: {cwd: string; files: string[]
3197 unincludedFiles . push ( filePath ) ;
3298 }
3399
34- const fallbackTsConfigPath = path . join ( cwd , 'node_modules' , '.cache' , cacheDirName , 'tsconfig.xo.json' ) ;
35-
36- tsConfig . files = unincludedFiles ;
37-
38- if ( unincludedFiles . length > 0 ) {
39- try {
40- await fs . writeFile ( fallbackTsConfigPath , JSON . stringify ( tsConfig , null , 2 ) ) ;
41- } catch ( error ) {
42- console . error ( error ) ;
43- }
100+ if ( unincludedFiles . length === 0 ) {
101+ return { unincludedFiles, program : undefined } ;
44102 }
45103
46- return { unincludedFiles, fallbackTsConfigPath} ;
104+ const existingFiles = unincludedFiles . filter ( file => fs . existsSync ( file ) ) ;
105+ // TypeScript will surface opaque diagnostics for missing files; pre-filter so we only pay the program cost for real files.
106+
107+ return {
108+ unincludedFiles,
109+ program : createInMemoryProgram ( existingFiles , cwd ) ,
110+ } ;
47111}
0 commit comments