77 */
88
99import type { OnLoadResult , PartialMessage , PartialNote , ResolveResult } from 'esbuild' ;
10+ import { existsSync } from 'node:fs' ;
1011import { dirname , join } from 'node:path' ;
1112import { fileURLToPath , pathToFileURL } from 'node:url' ;
12- import type { CanonicalizeContext , CompileResult , Exception , Syntax } from 'sass-embedded' ;
13+ import type { CompileResult , Exception , Syntax } from 'sass-embedded' ;
1314import type { SassCompiler } from '../../sass/sass-service' ;
1415import { MemoryCache } from '../cache' ;
1516import { StylesheetLanguage , StylesheetPluginOptions } from './stylesheet-plugin-factory' ;
@@ -18,6 +19,7 @@ let sassService: SassCompiler | undefined;
1819let sassServicePromise : Promise < SassCompiler > | undefined ;
1920let resolutionCache : MemoryCache < URL | null > | undefined ;
2021let packageRootCache : MemoryCache < string | null > | undefined ;
22+ let nodeModulesChainCache : Map < string , string > | undefined ;
2123
2224function isSassException ( error : unknown ) : error is Exception {
2325 return ! ! error && typeof error === 'object' && 'sassMessage' in error ;
@@ -26,6 +28,7 @@ function isSassException(error: unknown): error is Exception {
2628export function resetSassWorkerPoolCaches ( ) : void {
2729 resolutionCache ?. clear ( ) ;
2830 packageRootCache ?. clear ( ) ;
31+ nodeModulesChainCache ?. clear ( ) ;
2932 if ( sassService ) {
3033 sassService . clearCache ( ) ;
3134 } else if ( sassServicePromise ) {
@@ -50,12 +53,7 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
5053 fileFilter : / \. s [ a c ] s s $ / ,
5154 process ( data , file , format , options , build ) {
5255 const syntax = format === 'sass' ? 'indented' : 'scss' ;
53- const resolveUrl = async ( url : string , options : CanonicalizeContext ) => {
54- let resolveDir = build . initialOptions . absWorkingDir ;
55- if ( options . containingUrl ) {
56- resolveDir = dirname ( fileURLToPath ( options . containingUrl ) ) ;
57- }
58-
56+ const resolveUrl = async ( url : string , resolveDir : string | undefined ) => {
5957 const path = url . startsWith ( 'pkg:' ) ? url . slice ( 4 ) : url ;
6058 const result = await build . resolve ( path , {
6159 kind : 'import-rule' ,
@@ -65,7 +63,14 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
6563 return result ;
6664 } ;
6765
68- return compileString ( data , file , syntax , options , resolveUrl ) ;
66+ return compileString (
67+ data ,
68+ file ,
69+ syntax ,
70+ options ,
71+ resolveUrl ,
72+ build . initialOptions . absWorkingDir ,
73+ ) ;
6974 } ,
7075} ) ;
7176
@@ -97,12 +102,39 @@ function parsePackageName(url: string): { packageName: string; readonly pathSegm
97102 } ;
98103}
99104
105+ /**
106+ * Builds a key describing the `node_modules` directories visible from a directory, in the order
107+ * that package resolution searches them. Two directories with an identical key resolve every
108+ * package specifier to the same location; two with differing keys may not.
109+ *
110+ * The result is cached for the directory and for each of its parents.
111+ *
112+ * @param directory An absolute path of the directory to analyze.
113+ * @returns A key representing the `node_modules` directories visible from the directory.
114+ */
115+ function nodeModulesChainKey ( directory : string ) : string {
116+ nodeModulesChainCache ??= new Map < string , string > ( ) ;
117+ const cached = nodeModulesChainCache . get ( directory ) ;
118+ if ( cached !== undefined ) {
119+ return cached ;
120+ }
121+
122+ const parent = dirname ( directory ) ;
123+ const parentKey = parent === directory ? '' : nodeModulesChainKey ( parent ) ;
124+ const candidate = join ( directory , 'node_modules' ) ;
125+ const key = existsSync ( candidate ) ? `${ candidate } \n${ parentKey } ` : parentKey ;
126+ nodeModulesChainCache . set ( directory , key ) ;
127+
128+ return key ;
129+ }
130+
100131async function compileString (
101132 data : string ,
102133 filePath : string ,
103134 syntax : Syntax ,
104135 options : StylesheetPluginOptions ,
105- resolveUrl : ( url : string , options : CanonicalizeContext ) => Promise < ResolveResult > ,
136+ resolveUrl : ( url : string , resolveDir : string | undefined ) => Promise < ResolveResult > ,
137+ workingDirectory : string | undefined ,
106138) : Promise < OnLoadResult > {
107139 // Lazily load Sass when a Sass file is found
108140 if ( sassService === undefined ) {
@@ -119,7 +151,9 @@ async function compileString(
119151 }
120152
121153 // Caching follows Sass behavior where a given package url will always resolve to the same value
122- // regardless of its importer's path. Relative paths are qualified with the containing URL.
154+ // for importers that search the same `node_modules` directories. Package urls are therefore
155+ // qualified with the visible `node_modules` directories of the importer instead of its path,
156+ // while relative paths are qualified with the containing URL.
123157 // A null value indicates that the cached resolution attempt failed to find a location and
124158 // later stage resolution should be attempted. This avoids potentially expensive repeat
125159 // failing resolution attempts.
@@ -145,11 +179,18 @@ async function compileString(
145179 importers : [
146180 {
147181 findFileUrl : ( url , options ) => {
182+ const resolveDir = options . containingUrl
183+ ? dirname ( fileURLToPath ( options . containingUrl ) )
184+ : workingDirectory ;
148185 const isPackage = isPackageUrl ( url ) ;
149- const cacheKey = isPackage ? url : `${ options . containingUrl ?. href ?? '' } :${ url } ` ;
186+ const chainKey =
187+ isPackage && resolveDir !== undefined ? nodeModulesChainKey ( resolveDir ) : '' ;
188+ const cacheKey = isPackage
189+ ? `${ chainKey } :${ url } `
190+ : `${ options . containingUrl ?. href ?? '' } :${ url } ` ;
150191
151192 return currentResolutionCache . getOrCreate ( cacheKey , async ( ) => {
152- const result = await resolveUrl ( url , options ) ;
193+ const result = await resolveUrl ( url , resolveDir ) ;
153194 if ( result . path ) {
154195 return pathToFileURL ( result . path ) ;
155196 }
@@ -164,10 +205,10 @@ async function compileString(
164205 // Caching package root locations is particularly beneficial for `@material/*` packages
165206 // which extensively use deep imports.
166207 const packageRoot = await currentPackageRootCache . getOrCreate (
167- packageName ,
208+ ` ${ chainKey } : ${ packageName } ` ,
168209 async ( ) => {
169210 // Use the required presence of a package root `package.json` file to resolve the location
170- const packageResult = await resolveUrl ( packageName + '/package.json' , options ) ;
211+ const packageResult = await resolveUrl ( packageName + '/package.json' , resolveDir ) ;
171212
172213 return packageResult . path ? dirname ( packageResult . path ) : null ;
173214 } ,
0 commit comments