@@ -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,35 +36,55 @@ 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
44
43
export class TSService {
45
44
private readonly watch : ts . WatchOfConfigFile < ts . BuilderProgram > ;
46
45
46
+ private readonly tsconfigPath : string ;
47
+
47
48
public readonly extraFileExtensions : string [ ] ;
48
49
49
50
private currTarget = {
50
51
code : "" ,
51
52
filePath : "" ,
53
+ dirMap : new Map < string , { name : string ; path : string } > ( ) ,
52
54
} ;
53
55
54
56
private readonly fileWatchCallbacks = new Map < string , ( ) => void > ( ) ;
55
57
56
58
public constructor ( tsconfigPath : string , extraFileExtensions : string [ ] ) {
59
+ this . tsconfigPath = tsconfigPath ;
57
60
this . extraFileExtensions = extraFileExtensions ;
58
61
this . watch = this . createWatch ( tsconfigPath , extraFileExtensions ) ;
59
62
}
60
63
61
64
public getProgram ( code : string , filePath : string ) : ts . Program {
62
- const lastTargetFilePath = this . currTarget . filePath ;
65
+ const normalized = normalizeFileName (
66
+ toRealFileName ( filePath , this . extraFileExtensions )
67
+ ) ;
68
+ const lastTarget = this . currTarget ;
69
+
70
+ const dirMap = new Map < string , { name : string ; path : string } > ( ) ;
71
+ let childPath = normalized ;
72
+ for ( const dirName of iterateDirs ( normalized ) ) {
73
+ dirMap . set ( dirName , { path : childPath , name : path . basename ( childPath ) } ) ;
74
+ childPath = dirName ;
75
+ }
63
76
this . currTarget = {
64
77
code,
65
- filePath,
78
+ filePath : normalized ,
79
+ dirMap,
66
80
} ;
67
- const refreshTargetPaths = [ filePath , lastTargetFilePath ] . filter ( ( s ) => s ) ;
68
- for ( const targetPath of refreshTargetPaths ) {
81
+ for ( const { filePath : targetPath } of [ this . currTarget , lastTarget ] ) {
82
+ if ( ! targetPath ) continue ;
83
+ if ( ! ts . sys . fileExists ( targetPath ) ) {
84
+ // Signal a directory change to request a re-scan of the directory
85
+ // because it targets a file that does not actually exist.
86
+ this . fileWatchCallbacks . get ( normalizeFileName ( this . tsconfigPath ) ) ?.( ) ;
87
+ }
69
88
getFileNamesIncludingVirtualTSX (
70
89
targetPath ,
71
90
this . extraFileExtensions
@@ -84,9 +103,7 @@ export class TSService {
84
103
tsconfigPath : string ,
85
104
extraFileExtensions : string [ ]
86
105
) : ts . WatchOfConfigFile < ts . BuilderProgram > {
87
- const normalizedTsconfigPaths = new Set ( [
88
- normalizeFileName ( toAbsolutePath ( tsconfigPath ) ) ,
89
- ] ) ;
106
+ const normalizedTsconfigPaths = new Set ( [ normalizeFileName ( tsconfigPath ) ] ) ;
90
107
const watchCompilerHost = ts . createWatchCompilerHost (
91
108
tsconfigPath ,
92
109
{
@@ -120,17 +137,41 @@ export class TSService {
120
137
fileExists : watchCompilerHost . fileExists ,
121
138
// eslint-disable-next-line @typescript-eslint/unbound-method -- Store original
122
139
readDirectory : watchCompilerHost . readDirectory ,
140
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Store original
141
+ directoryExists : watchCompilerHost . directoryExists ! ,
142
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Store original
143
+ getDirectories : watchCompilerHost . getDirectories ! ,
144
+ } ;
145
+ watchCompilerHost . getDirectories = ( dirName , ...args ) => {
146
+ return distinctArray (
147
+ ...original . getDirectories . call ( watchCompilerHost , dirName , ...args ) ,
148
+ // Include the path to the target file if the target file does not actually exist.
149
+ this . currTarget . dirMap . get ( normalizeFileName ( dirName ) ) ?. name
150
+ ) ;
123
151
} ;
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
- ] ;
152
+ watchCompilerHost . directoryExists = ( dirName , ...args ) => {
153
+ return (
154
+ original . directoryExists . call ( watchCompilerHost , dirName , ...args ) ||
155
+ // Include the path to the target file if the target file does not actually exist.
156
+ this . currTarget . dirMap . has ( normalizeFileName ( dirName ) )
157
+ ) ;
158
+ } ;
159
+ watchCompilerHost . readDirectory = ( dirName , ...args ) => {
160
+ const results = original . readDirectory . call (
161
+ watchCompilerHost ,
162
+ dirName ,
163
+ ...args
164
+ ) ;
165
+
166
+ // Include the target file if the target file does not actually exist.
167
+ const file = this . currTarget . dirMap . get ( normalizeFileName ( dirName ) ) ;
168
+ if ( file && file . path === this . currTarget . filePath ) {
169
+ results . push ( file . path ) ;
170
+ }
171
+
172
+ return distinctArray ( ...results ) . map ( ( result ) =>
173
+ toVirtualTSXFileName ( result , extraFileExtensions )
174
+ ) ;
134
175
} ;
135
176
watchCompilerHost . readFile = ( fileName , ...args ) => {
136
177
const realFileName = toRealFileName ( fileName , extraFileExtensions ) ;
@@ -151,12 +192,14 @@ export class TSService {
151
192
if ( ! code ) {
152
193
return code ;
153
194
}
195
+ // If it's tsconfig, it will take care of rewriting the `include`.
154
196
if ( normalizedTsconfigPaths . has ( normalized ) ) {
155
197
const configJson = ts . parseConfigFileTextToJson ( realFileName , code ) ;
156
198
if ( ! configJson . config ) {
157
199
return code ;
158
200
}
159
201
if ( configJson . config . extends ) {
202
+ // If it references another tsconfig, rewrite the `include` for that file as well.
160
203
for ( const extendConfigPath of [ configJson . config . extends ] . flat ( ) ) {
161
204
normalizedTsconfigPaths . add (
162
205
normalizeFileName (
@@ -184,12 +227,28 @@ export class TSService {
184
227
} ) ;
185
228
} ;
186
229
// Modify it so that it can be determined that the virtual file actually exists.
187
- watchCompilerHost . fileExists = ( fileName , ...args ) =>
188
- original . fileExists . call (
230
+ watchCompilerHost . fileExists = ( fileName , ...args ) => {
231
+ const normalizedFileName = normalizeFileName ( fileName ) ;
232
+
233
+ // Even if it is actually a file, if it is specified as a directory to the target file,
234
+ // it is assumed that it does not exist as a file.
235
+ if ( this . currTarget . dirMap . has ( normalizedFileName ) ) {
236
+ return false ;
237
+ }
238
+ const normalizedRealFileName = toRealFileName (
239
+ normalizedFileName ,
240
+ extraFileExtensions
241
+ ) ;
242
+ if ( this . currTarget . filePath === normalizedRealFileName ) {
243
+ // It is the file currently being parsed.
244
+ return true ;
245
+ }
246
+ return original . fileExists . call (
189
247
watchCompilerHost ,
190
248
toRealFileName ( fileName , extraFileExtensions ) ,
191
249
...args
192
250
) ;
251
+ } ;
193
252
194
253
// It keeps a callback to mark the parsed file as changed so that it can be reparsed.
195
254
watchCompilerHost . watchFile = ( fileName , callback ) => {
@@ -205,11 +264,13 @@ export class TSService {
205
264
} ;
206
265
} ;
207
266
// Use watchCompilerHost but don't actually watch the files and directories.
208
- watchCompilerHost . watchDirectory = ( ) => ( {
209
- close ( ) {
210
- // noop
211
- } ,
212
- } ) ;
267
+ watchCompilerHost . watchDirectory = ( ) => {
268
+ return {
269
+ close : ( ) => {
270
+ // noop
271
+ } ,
272
+ } ;
273
+ } ;
213
274
214
275
/**
215
276
* It heavily references typescript-eslint.
@@ -278,13 +339,32 @@ function normalizeFileName(fileName: string) {
278
339
normalized = normalized . slice ( 0 , - 1 ) ;
279
340
}
280
341
if ( ts . sys . useCaseSensitiveFileNames ) {
281
- return normalized ;
342
+ return toAbsolutePath ( normalized , null ) ;
282
343
}
283
- return normalized . toLowerCase ( ) ;
344
+ return toAbsolutePath ( normalized . toLowerCase ( ) , null ) ;
284
345
}
285
346
286
- function toAbsolutePath ( filePath : string , baseDir ? : string ) {
347
+ function toAbsolutePath ( filePath : string , baseDir : string | null ) {
287
348
return path . isAbsolute ( filePath )
288
349
? filePath
289
350
: path . join ( baseDir || process . cwd ( ) , filePath ) ;
290
351
}
352
+
353
+ function * iterateDirs ( filePath : string ) {
354
+ let target = filePath ;
355
+ let parent : string ;
356
+ while ( ( parent = path . dirname ( target ) ) !== target ) {
357
+ yield parent ;
358
+ target = parent ;
359
+ }
360
+ }
361
+
362
+ function distinctArray ( ...list : ( string | null | undefined ) [ ] ) {
363
+ return [
364
+ ...new Set (
365
+ ts . sys . useCaseSensitiveFileNames
366
+ ? list . map ( ( s ) => s ?. toLowerCase ( ) )
367
+ : list
368
+ ) ,
369
+ ] . filter ( ( s ) : s is string => s != null ) ;
370
+ }
0 commit comments