From f4be831197010a17394264bc74b1eb385ba95028 Mon Sep 17 00:00:00 2001
From: Alan Agius <17563226+alan-agius4@users.noreply.github.com>
Date: Fri, 14 Mar 2025 14:48:44 +0000
Subject: [PATCH] feat(@angular/build): Support Sass package importers

Enhanced Sass integration by adding support for package importers.

See: https://sass-lang.com/blog/announcing-pkg-importers/

Closes: #29854
---
 .../esbuild/stylesheets/sass-language.ts      |  7 ++--
 tests/legacy-cli/e2e.bzl                      |  1 +
 .../tests/build/styles/sass-pkg-importer.ts   | 34 +++++++++++++++++++
 3 files changed, 39 insertions(+), 3 deletions(-)
 create mode 100644 tests/legacy-cli/e2e/tests/build/styles/sass-pkg-importer.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 b3972ac407..4bccfb3adb 100644
--- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts
+++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts
@@ -43,7 +43,8 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
         resolveDir = dirname(fileURLToPath(options.containingUrl));
       }
 
-      const result = await build.resolve(url, {
+      const path = url.startsWith('pkg:') ? url.slice(4) : url;
+      const result = await build.resolve(path, {
         kind: 'import-rule',
         resolveDir,
       });
@@ -56,8 +57,8 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
 });
 
 function parsePackageName(url: string): { packageName: string; readonly pathSegments: string[] } {
-  const parts = url.split('/');
-  const hasScope = parts.length >= 2 && parts[0].startsWith('@');
+  const parts = (url.startsWith('pkg:') ? url.slice(4) : url).split('/');
+  const hasScope = parts.length >= 2 && parts[0][0] === '@';
   const [nameOrScope, nameOrFirstPath, ...pathPart] = parts;
   const packageName = hasScope ? `${nameOrScope}/${nameOrFirstPath}` : nameOrScope;
 
diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl
index 1bed11a775..2152f6dcd2 100644
--- a/tests/legacy-cli/e2e.bzl
+++ b/tests/legacy-cli/e2e.bzl
@@ -46,6 +46,7 @@ WEBPACK_IGNORE_TESTS = [
     "tests/i18n/ivy-localize-app-shell.js",
     "tests/i18n/ivy-localize-app-shell-service-worker.js",
     "tests/commands/serve/ssr-http-requests-assets.js",
+    "tests/build/styles/sass-pkg-importer.js",
     "tests/build/prerender/http-requests-assets.js",
     "tests/build/prerender/error-with-sourcemaps.js",
     "tests/build/server-rendering/server-routes-*",
diff --git a/tests/legacy-cli/e2e/tests/build/styles/sass-pkg-importer.ts b/tests/legacy-cli/e2e/tests/build/styles/sass-pkg-importer.ts
new file mode 100644
index 0000000000..8fbb6e7431
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/build/styles/sass-pkg-importer.ts
@@ -0,0 +1,34 @@
+import assert from 'node:assert';
+import { writeFile } from '../../../utils/fs';
+import { getActivePackageManager, uninstallPackage } from '../../../utils/packages';
+import { ng } from '../../../utils/process';
+import { isPrereleaseCli, updateJsonFile } from '../../../utils/project';
+import { appendFile } from 'node:fs/promises';
+import { getGlobalVariable } from '../../../utils/env';
+
+export default async function () {
+  assert(
+    getGlobalVariable('argv')['esbuild'],
+    'This test should not be called in the Webpack suite.',
+  );
+
+  // forcibly remove in case another test doesn't clean itself up
+  await uninstallPackage('@angular/material');
+
+  const isPrerelease = await isPrereleaseCli();
+  const tag = isPrerelease ? '@next' : '';
+  if (getActivePackageManager() === 'npm') {
+    await appendFile('.npmrc', '\nlegacy-peer-deps=true');
+  }
+
+  await ng('add', `@angular/material${tag}`, '--skip-confirmation');
+  await Promise.all([
+    updateJsonFile('angular.json', (workspaceJson) => {
+      const appArchitect = workspaceJson.projects['test-project'].architect;
+      appArchitect.build.options.styles = ['src/styles.scss'];
+    }),
+    writeFile('src/styles.scss', `@use 'pkg:@angular/material' as mat;`),
+  ]);
+
+  await ng('build');
+}