diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index ad2961cc2b2e..537b3004c4b3 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -16,12 +16,16 @@ import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin let sassService: SassCompiler | undefined; let sassServicePromise: Promise | undefined; +let resolutionCache: MemoryCache | undefined; +let packageRootCache: MemoryCache | undefined; function isSassException(error: unknown): error is Exception { return !!error && typeof error === 'object' && 'sassMessage' in error; } export function shutdownSassWorkerPool(): void { + resolutionCache = undefined; + packageRootCache = undefined; if (sassService) { void sassService.close(); sassService = undefined; @@ -91,14 +95,15 @@ async function compileString( } } - // Cache is currently local to individual compile requests. - // Caching follows Sass behavior where a given url will always resolve to the same value - // regardless of its importer's path. + // Caching follows Sass behavior where a given package url will always resolve to the same value + // regardless of its importer's path. Relative paths are qualified with the containing URL. // A null value indicates that the cached resolution attempt failed to find a location and // later stage resolution should be attempted. This avoids potentially expensive repeat // failing resolution attempts. - const resolutionCache = new MemoryCache(); - const packageRootCache = new MemoryCache(); + resolutionCache ??= new MemoryCache(); + packageRootCache ??= new MemoryCache(); + const currentResolutionCache = resolutionCache; + const currentPackageRootCache = packageRootCache; const warnings: PartialMessage[] = []; const { silenceDeprecations, futureDeprecations, fatalDeprecations } = options.sass ?? {}; @@ -116,8 +121,12 @@ async function compileString( quietDeps: true, importers: [ { - findFileUrl: (url, options) => - resolutionCache.getOrCreate(url, async () => { + findFileUrl: (url, options) => { + const cacheKey = url.startsWith('pkg:') + ? url + : `${options.containingUrl?.href ?? ''}:${url}`; + + return currentResolutionCache.getOrCreate(cacheKey, async () => { const result = await resolveUrl(url, options); if (result.path) { return pathToFileURL(result.path); @@ -128,12 +137,15 @@ async function compileString( // Caching package root locations is particularly beneficial for `@material/*` packages // which extensively use deep imports. - const packageRoot = await packageRootCache.getOrCreate(packageName, async () => { - // Use the required presence of a package root `package.json` file to resolve the location - const packageResult = await resolveUrl(packageName + '/package.json', options); + const packageRoot = await currentPackageRootCache.getOrCreate( + packageName, + async () => { + // Use the required presence of a package root `package.json` file to resolve the location + const packageResult = await resolveUrl(packageName + '/package.json', options); - return packageResult.path ? dirname(packageResult.path) : null; - }); + return packageResult.path ? dirname(packageResult.path) : null; + }, + ); // Package not found could be because of an error or the specifier is intended to be found // via a later stage of the resolution process (`loadPaths`, etc.). @@ -145,7 +157,8 @@ async function compileString( // Not found return null; - }), + }); + }, }, ], logger: { diff --git a/packages/angular/build/src/tools/sass/sass-service.ts b/packages/angular/build/src/tools/sass/sass-service.ts index c3d6cf991526..8ce65dd7e2f2 100644 --- a/packages/angular/build/src/tools/sass/sass-service.ts +++ b/packages/angular/build/src/tools/sass/sass-service.ts @@ -45,6 +45,7 @@ function isFileImporter(value: Importers): value is FileImporter { export class SassCompiler { #asyncCompiler: AsyncCompiler | undefined; #asyncCompilerPromise: Promise | undefined; + readonly #directoryCache = new Map(); constructor(private readonly rebase = false) {} @@ -119,7 +120,7 @@ export class SassCompiler { (Importer<'async'> | FileImporter<'async'> | NodePackageImporter)[] | undefined; let loadPaths = options.loadPaths; const entryDirectory = url ? dirname(fileURLToPath(url)) : process.cwd(); - const directoryCache = new Map(); + const directoryCache = this.#directoryCache; const rebaseSourceMaps = options.sourceMap ? new Map() : undefined; if (importers?.length) { @@ -187,11 +188,20 @@ export class SassCompiler { return result; } + /** + * Clear the directory cache. + */ + clearCache(): void { + this.#directoryCache.clear(); + } + /** * Shutdown the Sass compiler. * @returns A void promise that resolves when closing is complete. */ async close(): Promise { + this.clearCache(); + if (this.#asyncCompilerPromise) { try { await this.#ensureAsyncCompiler();