Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
},
});

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] === '@';
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});