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(); + }); + }); +});