diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index 7cc3437fa323..edd0ae7d22f0 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -15,7 +15,10 @@ import { RebuildState, } from '../../tools/esbuild/bundler-execution-result'; import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files'; -import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; +import { + resetSassWorkerPoolCaches, + shutdownSassWorkerPool, +} from '../../tools/esbuild/stylesheets/sass-language'; import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils'; import { ChangedFiles } from '../../tools/esbuild/watcher'; import { shouldWatchRoot } from '../../utils/environment-options'; @@ -210,6 +213,8 @@ export async function* runEsBuildBuildAction( // Clear removed files from current watch files changes.removed.forEach((removedPath) => currentWatchFiles.delete(removedPath)); + resetSassWorkerPoolCaches(); + const rebuildState = result.createRebuildState(changes); result = await withProgress('Changes detected. Rebuilding...', () => action(rebuildState)); 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..3939e63b9035 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,25 @@ 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 resetSassWorkerPoolCaches(): void { + resolutionCache?.clear(); + packageRootCache?.clear(); + if (sassService) { + sassService.clearCache(); + } else if (sassServicePromise) { + void sassServicePromise.then((service) => service.clearCache()); + } +} + export function shutdownSassWorkerPool(): void { + resetSassWorkerPoolCaches(); if (sassService) { void sassService.close(); sassService = undefined; @@ -91,14 +104,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 +130,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 +146,16 @@ 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 packageRootKey = `${options.containingUrl?.href ?? ''}:${packageName}`; + const packageRoot = await currentPackageRootCache.getOrCreate( + packageRootKey, + 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 +167,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(); diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts b/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts index cd541faa9900..c7669597e922 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts @@ -70,6 +70,9 @@ export async function getStylesConfig(wco: WebpackConfigOptions): Promise { + sassImplementation.clearCache(); + }); compiler.hooks.shutdown.tap('sass-service', () => { void sassImplementation.close(); });