@@ -9,85 +9,115 @@ import { findUpFromUrl } from '../../../util/findUpFromUrl.js';
9
9
type Href = string ;
10
10
11
11
export class ConfigSearch {
12
- private searchCache = new Map < Href , Promise < URL | undefined > > ( ) ;
13
- private searchDirCache = new Map < Href , Promise < URL | undefined > > ( ) ;
14
- private searchPlacesByProtocol : Map < string , string [ ] > ;
12
+ /**
13
+ * Cache of search results.
14
+ */
15
+ #searchCache = new Map < Href , Promise < URL | undefined > > ( ) ;
16
+ /**
17
+ * The scanner to use to search for config files.
18
+ */
19
+ #scanner: DirConfigScanner ;
15
20
16
21
/**
17
22
* @param searchPlaces - The list of file names to search for.
18
23
* @param allowedExtensionsByProtocol - Map of allowed extensions by protocol, '*' is used to match all protocols.
19
24
* @param fs - The file system to use.
20
25
*/
21
26
constructor (
22
- readonly searchPlaces : readonly string [ ] ,
23
- readonly allowedExtensionsByProtocol : Map < string , readonly string [ ] > ,
24
- private fs : VFileSystem ,
27
+ searchPlaces : readonly string [ ] ,
28
+ allowedExtensionsByProtocol : Map < string , readonly string [ ] > ,
29
+ fs : VFileSystem ,
25
30
) {
26
- this . searchPlacesByProtocol = setupSearchPlacesByProtocol ( searchPlaces , allowedExtensionsByProtocol ) ;
27
- this . searchPlaces = this . searchPlacesByProtocol . get ( '*' ) || searchPlaces ;
31
+ this . #scanner = new DirConfigScanner ( searchPlaces , allowedExtensionsByProtocol , fs ) ;
28
32
}
29
33
30
34
searchForConfig ( searchFromURL : URL ) : Promise < URL | undefined > {
31
- const dirUrl = new URL ( '.' , searchFromURL ) ;
32
- const searchHref = dirUrl . href ;
33
- const searchCache = this . searchCache ;
34
- const cached = searchCache . get ( searchHref ) ;
35
+ const dirUrl = searchFromURL . pathname . endsWith ( '/' ) ? searchFromURL : new URL ( '.' , searchFromURL ) ;
36
+ return this . #findUp( dirUrl ) ;
37
+ }
38
+
39
+ clearCache ( ) {
40
+ this . #searchCache. clear ( ) ;
41
+ this . #scanner. clearCache ( ) ;
42
+ }
43
+
44
+ #findUp( fromDir : URL ) : Promise < URL | undefined > {
45
+ const searchDirCache = this . #searchCache;
46
+ const cached = searchDirCache . get ( fromDir . href ) ;
35
47
if ( cached ) {
36
48
return cached ;
37
49
}
38
-
39
- const toPatchCache : URL [ ] = [ ] ;
40
- const pFoundUrl = this . findUpConfigPath ( dirUrl , storeVisit ) ;
41
- this . searchCache . set ( searchHref , pFoundUrl ) ;
42
- const searchDirCache = this . searchDirCache ;
43
-
44
- const patch = async ( ) => {
45
- try {
46
- await pFoundUrl ;
47
- for ( const dir of toPatchCache ) {
48
- searchDirCache . set ( dir . href , searchDirCache . get ( dir . href ) || pFoundUrl ) ;
49
- searchCache . set ( dir . href , searchCache . get ( dir . href ) || pFoundUrl ) ;
50
- }
51
-
52
- const result = searchCache . get ( searchHref ) || pFoundUrl ;
53
- searchCache . set ( searchHref , result ) ;
54
- } catch {
55
- // ignore
56
- }
50
+ const visited : URL [ ] = [ ] ;
51
+ let result : Promise < URL | undefined > | undefined = undefined ;
52
+ const predicate = ( dir : URL ) => {
53
+ visit ( dir ) ;
54
+ return this . #scanner. scanDirForConfigFile ( dir ) ;
57
55
} ;
56
+ result = findUpFromUrl ( predicate , fromDir , { type : 'file' } ) ;
57
+ searchDirCache . set ( fromDir . href , result ) ;
58
+ visited . forEach ( ( dir ) => searchDirCache . set ( dir . href , result ) ) ;
59
+ return result ;
58
60
59
- patch ( ) ;
60
- return pFoundUrl ;
61
-
62
- function storeVisit ( dir : URL ) {
63
- toPatchCache . push ( dir ) ;
61
+ /**
62
+ * Record directories that are visited while walking up the directory tree.
63
+ * This will help speed up future searches.
64
+ * @param dir - the directory that was visited.
65
+ */
66
+ function visit ( dir : URL ) {
67
+ if ( ! result ) {
68
+ visited . push ( dir ) ;
69
+ return ;
70
+ }
71
+ searchDirCache . set ( dir . href , searchDirCache . get ( dir . href ) || result ) ;
64
72
}
65
73
}
74
+ }
66
75
67
- clearCache ( ) {
68
- this . searchCache . clear ( ) ;
69
- this . searchDirCache . clear ( ) ;
70
- }
71
-
72
- private findUpConfigPath ( cwd : URL , visit : ( dir : URL ) => void ) : Promise < URL | undefined > {
73
- const searchDirCache = this . searchDirCache ;
74
- const cached = searchDirCache . get ( cwd . href ) ;
75
- if ( cached ) return cached ;
76
+ /**
77
+ * A Scanner that searches for a config file in a directory. It caches the results to speed up future requests.
78
+ */
79
+ export class DirConfigScanner {
80
+ #searchDirCache = new Map < Href , Promise < URL | undefined > > ( ) ;
81
+ #searchPlacesByProtocol: Map < string , string [ ] > ;
82
+ #searchPlaces: readonly string [ ] ;
76
83
77
- return findUpFromUrl ( ( dir ) => this . hasConfig ( dir , visit ) , cwd , { type : 'file' } ) ;
84
+ /**
85
+ * @param searchPlaces - The list of file names to search for.
86
+ * @param allowedExtensionsByProtocol - Map of allowed extensions by protocol, '*' is used to match all protocols.
87
+ * @param fs - The file system to use.
88
+ */
89
+ constructor (
90
+ searchPlaces : readonly string [ ] ,
91
+ readonly allowedExtensionsByProtocol : Map < string , readonly string [ ] > ,
92
+ private fs : VFileSystem ,
93
+ ) {
94
+ this . #searchPlacesByProtocol = setupSearchPlacesByProtocol ( searchPlaces , allowedExtensionsByProtocol ) ;
95
+ this . #searchPlaces = this . #searchPlacesByProtocol. get ( '*' ) || searchPlaces ;
78
96
}
79
97
80
- private hasConfig ( dir : URL , visited : ( dir : URL ) => void ) : Promise < URL | undefined > {
81
- const cached = this . searchDirCache . get ( dir . href ) ;
82
- if ( cached ) return cached ;
83
- visited ( dir ) ;
98
+ clearCache ( ) {
99
+ this . #searchDirCache. clear ( ) ;
100
+ }
84
101
85
- const result = this . hasConfigDir ( dir ) ;
86
- this . searchDirCache . set ( dir . href , result ) ;
102
+ /**
103
+ *
104
+ * @param dir - the directory to search for a config file.
105
+ * @param visited - a callback to be called for each directory visited.
106
+ * @returns A promise that resolves to the url of the config file or `undefined`.
107
+ */
108
+ scanDirForConfigFile ( dir : URL ) : Promise < URL | undefined > {
109
+ const searchDirCache = this . #searchDirCache;
110
+ const href = dir . href ;
111
+ const cached = searchDirCache . get ( href ) ;
112
+ if ( cached ) {
113
+ return cached ;
114
+ }
115
+ const result = this . #scanDirForConfig( dir ) ;
116
+ searchDirCache . set ( href , result ) ;
87
117
return result ;
88
118
}
89
119
90
- private createHasFileDirSearch ( ) : ( file : URL ) => Promise < boolean > {
120
+ # createHasFileDirSearch( ) : ( file : URL ) => Promise < boolean > {
91
121
const dirInfoCache = createAutoResolveCache < Href , Promise < Map < string , VfsDirEntry > > > ( ) ;
92
122
93
123
const hasFile = async ( filename : URL ) : Promise < boolean > => {
@@ -102,7 +132,7 @@ export class ConfigSearch {
102
132
if ( ! found ?. isDirectory ( ) && ! found ?. isSymbolicLink ( ) ) return false ;
103
133
}
104
134
const dirUrlHref = dir . href ;
105
- const dirInfo = await dirInfoCache . get ( dirUrlHref , async ( ) => await this . readDir ( dir ) ) ;
135
+ const dirInfo = await dirInfoCache . get ( dirUrlHref , async ( ) => await this . # readDir( dir ) ) ;
106
136
107
137
const name = urlBasename ( filename ) ;
108
138
const found = dirInfo . get ( name ) ;
@@ -112,7 +142,7 @@ export class ConfigSearch {
112
142
return hasFile ;
113
143
}
114
144
115
- private async readDir ( dir : URL ) : Promise < Map < string , VfsDirEntry > > {
145
+ async # readDir( dir : URL ) : Promise < Map < string , VfsDirEntry > > {
116
146
try {
117
147
const dirInfo = await this . fs . readDirectory ( dir ) ;
118
148
return new Map ( dirInfo . map ( ( ent ) => [ ent . name , ent ] ) ) ;
@@ -121,7 +151,7 @@ export class ConfigSearch {
121
151
}
122
152
}
123
153
124
- private createHasFileStatCheck ( ) : ( file : URL ) => Promise < boolean > {
154
+ # createHasFileStatCheck( ) : ( file : URL ) => Promise < boolean > {
125
155
const hasFile = async ( filename : URL ) : Promise < boolean > => {
126
156
const stat = await this . fs . stat ( filename ) . catch ( ( ) => undefined ) ;
127
157
return ! ! stat ?. isFile ( ) ;
@@ -130,12 +160,17 @@ export class ConfigSearch {
130
160
return hasFile ;
131
161
}
132
162
133
- private async hasConfigDir ( dir : URL ) : Promise < URL | undefined > {
163
+ /**
164
+ * Scan the directory for the first matching config file.
165
+ * @param dir - url of the directory to scan.
166
+ * @returns A promise that resolves to the url of the config file or `undefined`.
167
+ */
168
+ async #scanDirForConfig( dir : URL ) : Promise < URL | undefined > {
134
169
const hasFile = this . fs . getCapabilities ( dir ) . readDirectory
135
- ? this . createHasFileDirSearch ( )
136
- : this . createHasFileStatCheck ( ) ;
170
+ ? this . # createHasFileDirSearch( )
171
+ : this . # createHasFileStatCheck( ) ;
137
172
138
- const searchPlaces = this . searchPlacesByProtocol . get ( dir . protocol ) || this . searchPlaces ;
173
+ const searchPlaces = this . # searchPlacesByProtocol. get ( dir . protocol ) || this . # searchPlaces;
139
174
140
175
for ( const searchPlace of searchPlaces ) {
141
176
const file = new URL ( searchPlace , dir ) ;
@@ -145,6 +180,7 @@ export class ConfigSearch {
145
180
if ( await checkPackageJson ( this . fs , file ) ) return file ;
146
181
}
147
182
}
183
+
148
184
return undefined ;
149
185
}
150
186
}
0 commit comments