8
8
9
9
import { RawSourceMap } from '@ampproject/remapping' ;
10
10
import MagicString from 'magic-string' ;
11
- import { Dirent , readFileSync , readdirSync } from 'node:fs' ;
11
+ import { readFileSync , readdirSync } from 'node:fs' ;
12
12
import { basename , dirname , extname , join , relative } from 'node:path' ;
13
13
import { fileURLToPath , pathToFileURL } from 'node:url' ;
14
14
import type { FileImporter , Importer , ImporterResult , Syntax } from 'sass' ;
@@ -19,6 +19,15 @@ import type { FileImporter, Importer, ImporterResult, Syntax } from 'sass';
19
19
*/
20
20
const URL_REGEXP = / u r l (?: \( \s * ( [ ' " ] ? ) ) ( .* ?) (?: \1\s * \) ) / g;
21
21
22
+ /**
23
+ * A preprocessed cache entry for the files and directories within a previously searched
24
+ * directory when performing Sass import resolution.
25
+ */
26
+ export interface DirectoryEntry {
27
+ files : Set < string > ;
28
+ directories : Set < string > ;
29
+ }
30
+
22
31
/**
23
32
* A Sass Importer base class that provides the load logic to rebase all `url()` functions
24
33
* within a stylesheet. The rebasing will ensure that the URLs in the output of the Sass compiler
@@ -115,7 +124,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
115
124
export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
116
125
constructor (
117
126
entryDirectory : string ,
118
- private directoryCache = new Map < string , Dirent [ ] > ( ) ,
127
+ private directoryCache = new Map < string , DirectoryEntry > ( ) ,
119
128
rebaseSourceMaps ?: Map < string , RawSourceMap > ,
120
129
) {
121
130
super ( entryDirectory , rebaseSourceMaps ) ;
@@ -149,17 +158,6 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
149
158
// Remove the style extension if present to allow adding the `.import` suffix
150
159
const filename = basename ( stylesheetPath , hasStyleExtension ? extension : undefined ) ;
151
160
152
- let entries ;
153
- try {
154
- entries = this . directoryCache . get ( directory ) ;
155
- if ( ! entries ) {
156
- entries = readdirSync ( directory , { withFileTypes : true } ) ;
157
- this . directoryCache . set ( directory , entries ) ;
158
- }
159
- } catch {
160
- return null ;
161
- }
162
-
163
161
const importPotentials = new Set < string > ( ) ;
164
162
const defaultPotentials = new Set < string > ( ) ;
165
163
@@ -187,47 +185,82 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
187
185
defaultPotentials . add ( '_' + filename + '.css' ) ;
188
186
}
189
187
190
- const foundDefaults : string [ ] = [ ] ;
191
- const foundImports : string [ ] = [ ] ;
188
+ let foundDefaults ;
189
+ let foundImports ;
192
190
let hasPotentialIndex = false ;
193
- for ( const entry of entries ) {
194
- // Record if the name should be checked as a directory with an index file
195
- if ( checkDirectory && ! hasStyleExtension && entry . name === filename && entry . isDirectory ( ) ) {
196
- hasPotentialIndex = true ;
197
- }
198
191
199
- if ( ! entry . isFile ( ) ) {
200
- continue ;
192
+ let cachedEntries = this . directoryCache . get ( directory ) ;
193
+ if ( cachedEntries ) {
194
+ // If there is a preprocessed cache of the directory, perform an intersection of the potentials
195
+ // and the directory files.
196
+ const { files, directories } = cachedEntries ;
197
+ foundDefaults = [ ...defaultPotentials ] . filter ( ( potential ) => files . has ( potential ) ) ;
198
+ foundImports = [ ...importPotentials ] . filter ( ( potential ) => files . has ( potential ) ) ;
199
+ hasPotentialIndex = checkDirectory && ! hasStyleExtension && directories . has ( filename ) ;
200
+ } else {
201
+ // If no preprocessed cache exists, get the entries from the file system and, while searching,
202
+ // generate the cache for later requests.
203
+ let entries ;
204
+ try {
205
+ entries = readdirSync ( directory , { withFileTypes : true } ) ;
206
+ } catch {
207
+ return null ;
201
208
}
202
209
203
- if ( importPotentials . has ( entry . name ) ) {
204
- foundImports . push ( join ( directory , entry . name ) ) ;
205
- }
210
+ foundDefaults = [ ] ;
211
+ foundImports = [ ] ;
212
+ cachedEntries = { files : new Set < string > ( ) , directories : new Set < string > ( ) } ;
213
+ for ( const entry of entries ) {
214
+ const isDirectory = entry . isDirectory ( ) ;
215
+ if ( isDirectory ) {
216
+ cachedEntries . directories . add ( entry . name ) ;
217
+ }
218
+
219
+ // Record if the name should be checked as a directory with an index file
220
+ if ( checkDirectory && ! hasStyleExtension && entry . name === filename && isDirectory ) {
221
+ hasPotentialIndex = true ;
222
+ }
223
+
224
+ if ( ! entry . isFile ( ) ) {
225
+ continue ;
226
+ }
206
227
207
- if ( defaultPotentials . has ( entry . name ) ) {
208
- foundDefaults . push ( join ( directory , entry . name ) ) ;
228
+ cachedEntries . files . add ( entry . name ) ;
229
+
230
+ if ( importPotentials . has ( entry . name ) ) {
231
+ foundImports . push ( entry . name ) ;
232
+ }
233
+
234
+ if ( defaultPotentials . has ( entry . name ) ) {
235
+ foundDefaults . push ( entry . name ) ;
236
+ }
209
237
}
238
+
239
+ this . directoryCache . set ( directory , cachedEntries ) ;
210
240
}
211
241
212
242
// `foundImports` will only contain elements if `options.fromImport` is true
213
243
const result = this . checkFound ( foundImports ) ?? this . checkFound ( foundDefaults ) ;
244
+ if ( result !== null ) {
245
+ return pathToFileURL ( join ( directory , result ) ) ;
246
+ }
214
247
215
- if ( result === null && hasPotentialIndex ) {
248
+ if ( hasPotentialIndex ) {
216
249
// Check for index files using filename as a directory
217
250
return this . resolveImport ( url + '/index' , fromImport , false ) ;
218
251
}
219
252
220
- return result ;
253
+ return null ;
221
254
}
222
255
223
256
/**
224
257
* Checks an array of potential stylesheet files to determine if there is a valid
225
258
* stylesheet file. More than one discovered file may indicate an error.
226
259
* @param found An array of discovered stylesheet files.
227
- * @returns A fully resolved URL for a stylesheet file or `null` if not found.
260
+ * @returns A fully resolved path for a stylesheet file or `null` if not found.
228
261
* @throws If there are ambiguous files discovered.
229
262
*/
230
- private checkFound ( found : string [ ] ) : URL | null {
263
+ private checkFound ( found : string [ ] ) : string | null {
231
264
if ( found . length === 0 ) {
232
265
// Not found
233
266
return null ;
@@ -245,10 +278,10 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
245
278
246
279
// Return the non-CSS file (sass/scss files have priority)
247
280
// https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart#L44-L47
248
- return pathToFileURL ( foundWithoutCss [ 0 ] ) ;
281
+ return foundWithoutCss [ 0 ] ;
249
282
}
250
283
251
- return pathToFileURL ( found [ 0 ] ) ;
284
+ return found [ 0 ] ;
252
285
}
253
286
}
254
287
@@ -260,7 +293,7 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
260
293
export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
261
294
constructor (
262
295
entryDirectory : string ,
263
- directoryCache : Map < string , Dirent [ ] > ,
296
+ directoryCache : Map < string , DirectoryEntry > ,
264
297
rebaseSourceMaps : Map < string , RawSourceMap > | undefined ,
265
298
private finder : FileImporter < 'sync' > [ 'findFileUrl' ] ,
266
299
) {
@@ -286,7 +319,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
286
319
export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
287
320
constructor (
288
321
entryDirectory : string ,
289
- directoryCache : Map < string , Dirent [ ] > ,
322
+ directoryCache : Map < string , DirectoryEntry > ,
290
323
rebaseSourceMaps : Map < string , RawSourceMap > | undefined ,
291
324
private loadPaths : Iterable < string > ,
292
325
) {
0 commit comments