88
99import { RawSourceMap } from '@ampproject/remapping' ;
1010import MagicString from 'magic-string' ;
11- import { Dirent , readFileSync , readdirSync } from 'node:fs' ;
11+ import { readFileSync , readdirSync } from 'node:fs' ;
1212import { basename , dirname , extname , join , relative } from 'node:path' ;
1313import { fileURLToPath , pathToFileURL } from 'node:url' ;
1414import type { FileImporter , Importer , ImporterResult , Syntax } from 'sass' ;
@@ -19,6 +19,15 @@ import type { FileImporter, Importer, ImporterResult, Syntax } from 'sass';
1919 */
2020const URL_REGEXP = / u r l (?: \( \s * ( [ ' " ] ? ) ) ( .* ?) (?: \1\s * \) ) / g;
2121
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+
2231/**
2332 * A Sass Importer base class that provides the load logic to rebase all `url()` functions
2433 * 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'> {
115124export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
116125 constructor (
117126 entryDirectory : string ,
118- private directoryCache = new Map < string , Dirent [ ] > ( ) ,
127+ private directoryCache = new Map < string , DirectoryEntry > ( ) ,
119128 rebaseSourceMaps ?: Map < string , RawSourceMap > ,
120129 ) {
121130 super ( entryDirectory , rebaseSourceMaps ) ;
@@ -149,17 +158,6 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
149158 // Remove the style extension if present to allow adding the `.import` suffix
150159 const filename = basename ( stylesheetPath , hasStyleExtension ? extension : undefined ) ;
151160
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-
163161 const importPotentials = new Set < string > ( ) ;
164162 const defaultPotentials = new Set < string > ( ) ;
165163
@@ -187,47 +185,82 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
187185 defaultPotentials . add ( '_' + filename + '.css' ) ;
188186 }
189187
190- const foundDefaults : string [ ] = [ ] ;
191- const foundImports : string [ ] = [ ] ;
188+ let foundDefaults ;
189+ let foundImports ;
192190 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- }
198191
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 ;
201208 }
202209
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+ }
206227
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+ }
209237 }
238+
239+ this . directoryCache . set ( directory , cachedEntries ) ;
210240 }
211241
212242 // `foundImports` will only contain elements if `options.fromImport` is true
213243 const result = this . checkFound ( foundImports ) ?? this . checkFound ( foundDefaults ) ;
244+ if ( result !== null ) {
245+ return pathToFileURL ( join ( directory , result ) ) ;
246+ }
214247
215- if ( result === null && hasPotentialIndex ) {
248+ if ( hasPotentialIndex ) {
216249 // Check for index files using filename as a directory
217250 return this . resolveImport ( url + '/index' , fromImport , false ) ;
218251 }
219252
220- return result ;
253+ return null ;
221254 }
222255
223256 /**
224257 * Checks an array of potential stylesheet files to determine if there is a valid
225258 * stylesheet file. More than one discovered file may indicate an error.
226259 * @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.
228261 * @throws If there are ambiguous files discovered.
229262 */
230- private checkFound ( found : string [ ] ) : URL | null {
263+ private checkFound ( found : string [ ] ) : string | null {
231264 if ( found . length === 0 ) {
232265 // Not found
233266 return null ;
@@ -245,10 +278,10 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
245278
246279 // Return the non-CSS file (sass/scss files have priority)
247280 // https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart#L44-L47
248- return pathToFileURL ( foundWithoutCss [ 0 ] ) ;
281+ return foundWithoutCss [ 0 ] ;
249282 }
250283
251- return pathToFileURL ( found [ 0 ] ) ;
284+ return found [ 0 ] ;
252285 }
253286}
254287
@@ -260,7 +293,7 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
260293export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
261294 constructor (
262295 entryDirectory : string ,
263- directoryCache : Map < string , Dirent [ ] > ,
296+ directoryCache : Map < string , DirectoryEntry > ,
264297 rebaseSourceMaps : Map < string , RawSourceMap > | undefined ,
265298 private finder : FileImporter < 'sync' > [ 'findFileUrl' ] ,
266299 ) {
@@ -286,7 +319,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
286319export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
287320 constructor (
288321 entryDirectory : string ,
289- directoryCache : Map < string , Dirent [ ] > ,
322+ directoryCache : Map < string , DirectoryEntry > ,
290323 rebaseSourceMaps : Map < string , RawSourceMap > | undefined ,
291324 private loadPaths : Iterable < string > ,
292325 ) {
0 commit comments