From 365375f6e0e836564c5f05a48fe2a023e5d22a8c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:40:24 +0000 Subject: [PATCH] perf(@angular/build): key Sass package resolutions without containing URL qualification Previously, only pkg: URLs were treated as package specifiers when constructing the Sass resolutionCache key. Bare package specifiers such as @angular/material were qualified with the importing file's containing URL, resulting in cache misses across different component stylesheets. Additionally, packageRootCache was also unnecessarily qualified with the containing URL. Package specifiers are now detected and cached without containing URL qualification, allowing package resolutions and package roots to be shared across all stylesheets. --- .../esbuild/stylesheets/sass-language.ts | 26 ++++++++-- .../esbuild/stylesheets/sass-language_spec.ts | 49 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts 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 3939e63b9035..cce85f61b9a2 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -69,6 +69,20 @@ export const SassStylesheetLanguage = Object.freeze({ }, }); +export function isPackageUrl(url: string): boolean { + if (url.startsWith('pkg:')) { + return true; + } + + return ( + url.length > 0 && + !url.startsWith('.') && + !url.startsWith('/') && + !url.startsWith('\\') && + !url.includes(':') + ); +} + function parsePackageName(url: string): { packageName: string; readonly pathSegments: string[] } { const parts = (url.startsWith('pkg:') ? url.slice(4) : url).split('/'); const hasScope = parts.length >= 2 && parts[0][0] === '@'; @@ -131,9 +145,8 @@ async function compileString( importers: [ { findFileUrl: (url, options) => { - const cacheKey = url.startsWith('pkg:') - ? url - : `${options.containingUrl?.href ?? ''}:${url}`; + const isPackage = isPackageUrl(url); + const cacheKey = isPackage ? url : `${options.containingUrl?.href ?? ''}:${url}`; return currentResolutionCache.getOrCreate(cacheKey, async () => { const result = await resolveUrl(url, options); @@ -142,13 +155,16 @@ async function compileString( } // Check for package deep imports + if (!isPackage) { + return null; + } + const { packageName, pathSegments } = parsePackageName(url); // Caching package root locations is particularly beneficial for `@material/*` packages // which extensively use deep imports. - const packageRootKey = `${options.containingUrl?.href ?? ''}:${packageName}`; const packageRoot = await currentPackageRootCache.getOrCreate( - packageRootKey, + 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); diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts new file mode 100644 index 000000000000..7be104741caa --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language_spec.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { isPackageUrl } from './sass-language'; + +describe('sass-language', () => { + describe('isPackageUrl', () => { + it('should identify pkg: scheme URLs as package URLs', () => { + expect(isPackageUrl('pkg:@angular/material')).toBeTrue(); + expect(isPackageUrl('pkg:bootstrap')).toBeTrue(); + expect(isPackageUrl('pkg:@material/button/button')).toBeTrue(); + }); + + it('should identify bare specifiers as package URLs', () => { + expect(isPackageUrl('@angular/material')).toBeTrue(); + expect(isPackageUrl('@angular/material/button')).toBeTrue(); + expect(isPackageUrl('@material/button/button.scss')).toBeTrue(); + expect(isPackageUrl('bootstrap')).toBeTrue(); + expect(isPackageUrl('bootstrap/scss/bootstrap')).toBeTrue(); + }); + + it('should not identify relative paths as package URLs', () => { + expect(isPackageUrl('./styles.scss')).toBeFalse(); + expect(isPackageUrl('../shared/variables')).toBeFalse(); + expect(isPackageUrl('.hidden')).toBeFalse(); + expect(isPackageUrl('.\\styles.scss')).toBeFalse(); + expect(isPackageUrl('..\\shared\\variables')).toBeFalse(); + }); + + it('should not identify absolute paths or non-pkg URLs as package URLs', () => { + expect(isPackageUrl('/styles/theme.scss')).toBeFalse(); + expect(isPackageUrl('\\styles\\theme.scss')).toBeFalse(); + expect(isPackageUrl('file:///path/to/theme.scss')).toBeFalse(); + expect(isPackageUrl('http://example.com/styles.css')).toBeFalse(); + expect(isPackageUrl('https://example.com/styles.css')).toBeFalse(); + expect(isPackageUrl('C:\\path\\to\\theme.scss')).toBeFalse(); + expect(isPackageUrl('C:/path/to/theme.scss')).toBeFalse(); + }); + + it('should not identify empty string as a package URL', () => { + expect(isPackageUrl('')).toBeFalse(); + }); + }); +});