@@ -18,7 +18,6 @@ export class TSServiceManager {
18
18
19
19
public getProgram ( code : string , options : ProgramOptions ) : ts . Program {
20
20
const tsconfigPath = options . project ;
21
- const fileName = normalizeFileName ( toAbsolutePath ( options . filePath ) ) ;
22
21
const extraFileExtensions = [ ...new Set ( options . extraFileExtensions ) ] ;
23
22
24
23
let serviceList = this . tsServices . get ( tsconfigPath ) ;
@@ -37,7 +36,7 @@ export class TSServiceManager {
37
36
serviceList . unshift ( service ) ;
38
37
}
39
38
40
- return service . getProgram ( code , fileName ) ;
39
+ return service . getProgram ( code , options . filePath ) ;
41
40
}
42
41
}
43
42
@@ -49,22 +48,59 @@ export class TSService {
49
48
private currTarget = {
50
49
code : "" ,
51
50
filePath : "" ,
51
+ dirMap : new Map < string , { name : string ; path : string } > ( ) ,
52
52
} ;
53
53
54
54
private readonly fileWatchCallbacks = new Map < string , ( ) => void > ( ) ;
55
55
56
+ private readonly dirWatchCallbacks = new Map < string , ( ) => void > ( ) ;
57
+
56
58
public constructor ( tsconfigPath : string , extraFileExtensions : string [ ] ) {
57
59
this . extraFileExtensions = extraFileExtensions ;
58
60
this . watch = this . createWatch ( tsconfigPath , extraFileExtensions ) ;
59
61
}
60
62
61
63
public getProgram ( code : string , filePath : string ) : ts . Program {
62
- const lastTargetFilePath = this . currTarget . filePath ;
64
+ const normalized = normalizeFileName (
65
+ toRealFileName ( filePath , this . extraFileExtensions )
66
+ ) ;
67
+ const lastTarget = this . currTarget ;
68
+
69
+ const dirMap = new Map < string , { name : string ; path : string } > ( ) ;
70
+ let childPath = normalized ;
71
+ for ( const dirName of iterateDirs ( normalized ) ) {
72
+ dirMap . set ( dirName , { path : childPath , name : path . basename ( childPath ) } ) ;
73
+ childPath = dirName ;
74
+ }
63
75
this . currTarget = {
64
76
code,
65
- filePath,
77
+ filePath : normalized ,
78
+ dirMap,
66
79
} ;
67
- const refreshTargetPaths = [ filePath , lastTargetFilePath ] . filter ( ( s ) => s ) ;
80
+ for ( const { filePath : targetPath , dirMap : map } of [
81
+ this . currTarget ,
82
+ lastTarget ,
83
+ ] ) {
84
+ if ( ! targetPath ) continue ;
85
+ if ( ts . sys . fileExists ( targetPath ) ) {
86
+ getFileNamesIncludingVirtualTSX (
87
+ targetPath ,
88
+ this . extraFileExtensions
89
+ ) . forEach ( ( vFilePath ) => {
90
+ this . fileWatchCallbacks . get ( vFilePath ) ?.( ) ;
91
+ } ) ;
92
+ } else {
93
+ // Signal a directory change to request a re-scan of the directory
94
+ // because it targets a file that does not actually exist.
95
+ for ( const dirName of map . keys ( ) ) {
96
+ this . dirWatchCallbacks . get ( dirName ) ?.( ) ;
97
+ }
98
+ }
99
+ }
100
+
101
+ const refreshTargetPaths = [ normalized , lastTarget . filePath ] . filter (
102
+ ( s ) => s
103
+ ) ;
68
104
for ( const targetPath of refreshTargetPaths ) {
69
105
getFileNamesIncludingVirtualTSX (
70
106
targetPath ,
@@ -84,9 +120,7 @@ export class TSService {
84
120
tsconfigPath : string ,
85
121
extraFileExtensions : string [ ]
86
122
) : ts . WatchOfConfigFile < ts . BuilderProgram > {
87
- const normalizedTsconfigPaths = new Set ( [
88
- normalizeFileName ( toAbsolutePath ( tsconfigPath ) ) ,
89
- ] ) ;
123
+ const normalizedTsconfigPaths = new Set ( [ normalizeFileName ( tsconfigPath ) ] ) ;
90
124
const watchCompilerHost = ts . createWatchCompilerHost (
91
125
tsconfigPath ,
92
126
{
@@ -120,17 +154,41 @@ export class TSService {
120
154
fileExists : watchCompilerHost . fileExists ,
121
155
// eslint-disable-next-line @typescript-eslint/unbound-method -- Store original
122
156
readDirectory : watchCompilerHost . readDirectory ,
157
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Store original
158
+ directoryExists : watchCompilerHost . directoryExists ! ,
159
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Store original
160
+ getDirectories : watchCompilerHost . getDirectories ! ,
161
+ } ;
162
+ watchCompilerHost . getDirectories = ( dirName , ...args ) => {
163
+ return distinctArray (
164
+ ...original . getDirectories . call ( watchCompilerHost , dirName , ...args ) ,
165
+ // Include the path to the target file if the target file does not actually exist.
166
+ this . currTarget . dirMap . get ( normalizeFileName ( dirName ) ) ?. name
167
+ ) ;
168
+ } ;
169
+ watchCompilerHost . directoryExists = ( dirName , ...args ) => {
170
+ return (
171
+ original . directoryExists . call ( watchCompilerHost , dirName , ...args ) ||
172
+ // Include the path to the target file if the target file does not actually exist.
173
+ this . currTarget . dirMap . has ( normalizeFileName ( dirName ) )
174
+ ) ;
123
175
} ;
124
- watchCompilerHost . readDirectory = ( ...args ) => {
125
- const results = original . readDirectory . call ( watchCompilerHost , ...args ) ;
126
-
127
- return [
128
- ...new Set (
129
- results . map ( ( result ) =>
130
- toVirtualTSXFileName ( result , extraFileExtensions )
131
- )
132
- ) ,
133
- ] ;
176
+ watchCompilerHost . readDirectory = ( dirName , ...args ) => {
177
+ const results = original . readDirectory . call (
178
+ watchCompilerHost ,
179
+ dirName ,
180
+ ...args
181
+ ) ;
182
+
183
+ // Include the target file if the target file does not actually exist.
184
+ const file = this . currTarget . dirMap . get ( normalizeFileName ( dirName ) ) ;
185
+ if ( file && file . path === this . currTarget . filePath ) {
186
+ results . push ( file . path ) ;
187
+ }
188
+
189
+ return distinctArray ( ...results ) . map ( ( result ) =>
190
+ toVirtualTSXFileName ( result , extraFileExtensions )
191
+ ) ;
134
192
} ;
135
193
watchCompilerHost . readFile = ( fileName , ...args ) => {
136
194
const realFileName = toRealFileName ( fileName , extraFileExtensions ) ;
@@ -151,12 +209,14 @@ export class TSService {
151
209
if ( ! code ) {
152
210
return code ;
153
211
}
212
+ // If it's tsconfig, it will take care of rewriting the `include`.
154
213
if ( normalizedTsconfigPaths . has ( normalized ) ) {
155
214
const configJson = ts . parseConfigFileTextToJson ( realFileName , code ) ;
156
215
if ( ! configJson . config ) {
157
216
return code ;
158
217
}
159
218
if ( configJson . config . extends ) {
219
+ // If it references another tsconfig, rewrite the `include` for that file as well.
160
220
for ( const extendConfigPath of [ configJson . config . extends ] . flat ( ) ) {
161
221
normalizedTsconfigPaths . add (
162
222
normalizeFileName (
@@ -184,12 +244,28 @@ export class TSService {
184
244
} ) ;
185
245
} ;
186
246
// Modify it so that it can be determined that the virtual file actually exists.
187
- watchCompilerHost . fileExists = ( fileName , ...args ) =>
188
- original . fileExists . call (
247
+ watchCompilerHost . fileExists = ( fileName , ...args ) => {
248
+ const normalizedFileName = normalizeFileName ( fileName ) ;
249
+
250
+ // Even if it is actually a file, if it is specified as a directory to the target file,
251
+ // it is assumed that it does not exist as a file.
252
+ if ( this . currTarget . dirMap . has ( normalizedFileName ) ) {
253
+ return false ;
254
+ }
255
+ const normalizedRealFileName = toRealFileName (
256
+ normalizedFileName ,
257
+ extraFileExtensions
258
+ ) ;
259
+ if ( this . currTarget . filePath === normalizedRealFileName ) {
260
+ // It is the file currently being parsed.
261
+ return true ;
262
+ }
263
+ return original . fileExists . call (
189
264
watchCompilerHost ,
190
265
toRealFileName ( fileName , extraFileExtensions ) ,
191
266
...args
192
267
) ;
268
+ } ;
193
269
194
270
// It keeps a callback to mark the parsed file as changed so that it can be reparsed.
195
271
watchCompilerHost . watchFile = ( fileName , callback ) => {
@@ -205,11 +281,15 @@ export class TSService {
205
281
} ;
206
282
} ;
207
283
// Use watchCompilerHost but don't actually watch the files and directories.
208
- watchCompilerHost . watchDirectory = ( ) => ( {
209
- close ( ) {
210
- // noop
211
- } ,
212
- } ) ;
284
+ watchCompilerHost . watchDirectory = ( dirName , callback ) => {
285
+ const normalized = normalizeFileName ( dirName ) ;
286
+ this . dirWatchCallbacks . set ( normalized , ( ) => callback ( dirName ) ) ;
287
+ return {
288
+ close : ( ) => {
289
+ this . dirWatchCallbacks . delete ( normalized ) ;
290
+ } ,
291
+ } ;
292
+ } ;
213
293
214
294
/**
215
295
* It heavily references typescript-eslint.
@@ -278,13 +358,32 @@ function normalizeFileName(fileName: string) {
278
358
normalized = normalized . slice ( 0 , - 1 ) ;
279
359
}
280
360
if ( ts . sys . useCaseSensitiveFileNames ) {
281
- return normalized ;
361
+ return toAbsolutePath ( normalized , null ) ;
282
362
}
283
- return normalized . toLowerCase ( ) ;
363
+ return toAbsolutePath ( normalized . toLowerCase ( ) , null ) ;
284
364
}
285
365
286
- function toAbsolutePath ( filePath : string , baseDir ? : string ) {
366
+ function toAbsolutePath ( filePath : string , baseDir : string | null ) {
287
367
return path . isAbsolute ( filePath )
288
368
? filePath
289
369
: path . join ( baseDir || process . cwd ( ) , filePath ) ;
290
370
}
371
+
372
+ function * iterateDirs ( filePath : string ) {
373
+ let target = filePath ;
374
+ let parent : string ;
375
+ while ( ( parent = path . dirname ( target ) ) !== target ) {
376
+ yield parent ;
377
+ target = parent ;
378
+ }
379
+ }
380
+
381
+ function distinctArray ( ...list : ( string | null | undefined ) [ ] ) {
382
+ return [
383
+ ...new Set (
384
+ ts . sys . useCaseSensitiveFileNames
385
+ ? list . map ( ( s ) => s ?. toLowerCase ( ) )
386
+ : list
387
+ ) ,
388
+ ] . filter ( ( s ) : s is string => s != null ) ;
389
+ }
0 commit comments