From 7d3844ad633b1f9b80e76014a52848d761b63510 Mon Sep 17 00:00:00 2001 From: Venkatesan <68438061+VenkatKwest@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:13:03 +0530 Subject: [PATCH 01/11] refactor(@angular-devkit/schematics): remove shell usage in git spawn to prevent command injection Git is a native executable on Windows and does not require shell: true. Switch to array-based spawn and separate the -m flag from the commit message to prevent command injection via crafted commit messages. (cherry picked from commit f1ed0257abad0f5772647911d0a2683e3e6b3eb8) --- .../angular_devkit/schematics/tasks/repo-init/executor.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/schematics/tasks/repo-init/executor.ts b/packages/angular_devkit/schematics/tasks/repo-init/executor.ts index 97b2b12a3619..607e1bfc5cba 100644 --- a/packages/angular_devkit/schematics/tasks/repo-init/executor.ts +++ b/packages/angular_devkit/schematics/tasks/repo-init/executor.ts @@ -29,7 +29,6 @@ export default function ( const errorStream = ignoreErrorStream ? 'ignore' : process.stderr; const spawnOptions: SpawnOptions = { stdio: [process.stdin, outputStream, errorStream], - shell: true, cwd: path.join(rootDirectory, options.workingDirectory || ''), env: { ...process.env, @@ -41,7 +40,7 @@ export default function ( }; return new Promise((resolve, reject) => { - spawn(`git ${args.join(' ')}`, spawnOptions).on('close', (code: number) => { + spawn('git', args, spawnOptions).on('close', (code: number) => { if (code === 0) { resolve(); } else { @@ -82,7 +81,7 @@ export default function ( if (options.commit) { const message = options.message || 'initial commit'; - await execute(['commit', `-m "${message}"`]); + await execute(['commit', '-m', message]); } context.logger.info('Successfully initialized git.'); From 8186faa117803ffb6ac8e2c4cd6ab7873502308d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:36:19 -0400 Subject: [PATCH 02/11] fix(@angular/build): ensure Vitest mock patching is executed only once Wrap the Vitest mocking overrides in a global guard to prevent repeated execution in shared environments or watch mode runs. (cherry picked from commit e558117b748ee5837324d49466108d21db596b2e) --- .../unit-test/runners/vitest/build-options.ts | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts index 9933f663aa86..27519844312a 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts @@ -157,31 +157,36 @@ export async function getVitestBuildOptions( const mockPatchContents = ` import { vi } from 'vitest'; - const error = new Error( - 'The "vi.mock" and related methods are not supported for relative imports with the Angular unit-test system. ' + - 'Please use Angular TestBed for mocking dependencies.' - ); - - // Store original implementations - const { mock, doMock, importMock, unmock, doUnmock } = vi; - - function patch(original) { - return (path, ...args) => { - // Check if the path is a string and starts with a character that indicates a relative path. - if (typeof path === 'string' && /^[./]/.test(path)) { - throw error; - } - - // Call the original function for non-relative paths. - return original(path, ...args); - }; + const ANGULAR_VITEST_MOCK_PATCH = Symbol.for('@angular/cli/vitest-mock-patch'); + if (!globalThis[ANGULAR_VITEST_MOCK_PATCH]) { + globalThis[ANGULAR_VITEST_MOCK_PATCH] = true; + + const error = new Error( + 'The "vi.mock" and related methods are not supported for relative imports with the Angular unit-test system. ' + + 'Please use Angular TestBed for mocking dependencies.' + ); + + // Store original implementations + const { mock, doMock, importMock, unmock, doUnmock } = vi; + + function patch(original) { + return (path, ...args) => { + // Check if the path is a string and starts with a character that indicates a relative path. + if (typeof path === 'string' && /^[./]/.test(path)) { + throw error; + } + + // Call the original function for non-relative paths. + return original(path, ...args); + }; + } + + vi.mock = patch(mock); + vi.doMock = patch(doMock); + vi.importMock = patch(importMock); + vi.unmock = patch(unmock); + vi.doUnmock = patch(doUnmock); } - - vi.mock = patch(mock); - vi.doMock = patch(doMock); - vi.importMock = patch(importMock); - vi.unmock = patch(unmock); - vi.doUnmock = patch(doUnmock); `; return { From 107d1a9e26fc59c7878254e563758818866f0f6e Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Fri, 27 Mar 2026 15:18:59 +0530 Subject: [PATCH 03/11] fix(@angular/build): preserve error stack traces during prerendering Reorder the nullish coalescing chain from `err.message ?? err.stack` to `err.stack ?? err.message` so that the full stack trace is preserved when available. Since `err.message` is almost always defined on Error objects, the previous order meant `err.stack` was never reached. Also add `assertIsError(err)` and consistent `err.code` inclusion across all three error-handling locations for improved type safety and debugging. Fixes #32503 (cherry picked from commit 81e4faae7699e2ed1eb8f4656dc115ca9c20f416) --- .../build/src/utils/server-rendering/prerender.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index f0e822eb3de9..1033a7575f88 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -116,8 +116,12 @@ export async function prerenderPages( sourcemap, outputMode, ).catch((err) => { + assertIsError(err); + return { - errors: [`An error occurred while extracting routes.\n\n${err.message ?? err.stack ?? err}`], + errors: [ + `An error occurred while extracting routes.\n\n${err.stack ?? err.message ?? err.code ?? err}`, + ], serializedRouteTree: [], appShellRoute: undefined, }; @@ -265,8 +269,9 @@ async function renderPages( } }) .catch((err) => { + assertIsError(err); errors.push( - `An error occurred while prerendering route '${route}'.\n\n${err.message ?? err.stack ?? err.code ?? err}`, + `An error occurred while prerendering route '${route}'.\n\n${err.stack ?? err.message ?? err.code ?? err}`, ); void renderWorker.destroy(); }); @@ -371,7 +376,7 @@ async function getAllRoutes( return { errors: [ - `An error occurred while extracting routes.\n\n${err.message ?? err.stack ?? err.code ?? err}`, + `An error occurred while extracting routes.\n\n${err.stack ?? err.message ?? err.code ?? err}`, ], serializedRouteTree: [], }; From 9136eb37630d6315891b3c881cd0ba4037c3254c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:50:15 -0400 Subject: [PATCH 04/11] fix(@angular/build): ensure transitive SCSS partial errors are tracked in watch mode When stylesheet bundling fails due to an error in a SCSS partial, we now ensure that `referencedFiles` are still passed to the `FileReferenceTracker`. This prevents the dependency between the component and the error file from being lost, allowing the component to be correctly rebuilt when the error is fixed. (cherry picked from commit 21d8aa4747573132476c3a0a4b7ea1f6405a71ef) --- .../behavior/rebuild-component_styles_spec.ts | 80 +++++++++++++++++++ .../tools/esbuild/angular/compiler-plugin.ts | 9 +++ 2 files changed, 89 insertions(+) diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts index 26ae35a8221f..08b683439684 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts @@ -58,5 +58,85 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { ]); }); } + + it('rebuilds component after error on rebuild from transitive import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace('app.component.css', 'app.component.scss'), + ); + await harness.writeFile('src/app/app.component.scss', "@import './a';"); + await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + + harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); + + // Introduce a syntax error + await harness.writeFile( + 'src/app/a.scss', + 'invalid-invalid-invalid\\nh1 { color: $primary; }', + ); + }, + async ({ result }) => { + expect(result?.success).toBe(false); + + // Fix the syntax error + await harness.writeFile('src/app/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + + harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); + }, + ]); + }); + + it('rebuilds component after error on rebuild from deep transitive import with partials', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace('app.component.css', 'app.component.scss'), + ); + await harness.writeFile('src/app/app.component.scss', "@import './intermediary';"); + await harness.writeFile('src/app/_intermediary.scss', "@import './partial';"); + await harness.writeFile('src/app/_partial.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + + harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); + + // Introduce a syntax error deeply + await harness.writeFile( + 'src/app/_partial.scss', + 'invalid-invalid-invalid\\nh1 { color: $primary; }', + ); + }, + async ({ result }) => { + expect(result?.success).toBe(false); + + // Fix the syntax error deeply + await harness.writeFile( + 'src/app/_partial.scss', + '$primary: blue;\\nh1 { color: $primary; }', + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + + harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); + }, + ]); + }); }); }); diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index af4dcaea01fb..1bcb8c40500a 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -220,10 +220,19 @@ export function createCompilerPlugin( if (stylesheetResult.errors) { (result.errors ??= []).push(...stylesheetResult.errors); + const { referencedFiles } = stylesheetResult; + if (referencedFiles) { + referencedFileTracker.add(containingFile, referencedFiles); + if (stylesheetFile) { + referencedFileTracker.add(stylesheetFile, referencedFiles); + } + } + return ''; } const { contents, outputFiles, metafile, referencedFiles } = stylesheetResult; + additionalResults.set(resultSource, { outputFiles, metafile, From ea14f28ccfc6e5534eaef516bf1bfbe21582da04 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:55:11 -0400 Subject: [PATCH 05/11] fix(@angular/cli): fix sourceRoot resolution for MCP projects tool Ensure that project.sourceRoot is used directly as it is already relative to the workspace root, preventing duplicated path prefixes for sub-projects. (cherry picked from commit 4815a5417c7a0135fb66149c2e4c1008e21e3a26) --- .../cli/src/commands/mcp/tools/projects.ts | 2 +- .../mcp/projects-sourceroot-resolution.ts | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/tests/mcp/projects-sourceroot-resolution.ts diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index 8c6eb5d332f6..c53adb7828df 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -467,7 +467,7 @@ async function loadAndParseWorkspace( const projects = []; const workspaceRoot = dirname(configFile); for (const [name, project] of ws.projects.entries()) { - const sourceRoot = posix.join(project.root, project.sourceRoot ?? 'src'); + const sourceRoot = project.sourceRoot ?? posix.join(project.root, 'src'); const fullSourceRoot = join(workspaceRoot, sourceRoot); const unitTestFramework = getUnitTestFramework(project.targets.get('test')); const styleLanguage = await getProjectStyleLanguage(project, ws, fullSourceRoot); diff --git a/tests/e2e/tests/mcp/projects-sourceroot-resolution.ts b/tests/e2e/tests/mcp/projects-sourceroot-resolution.ts new file mode 100644 index 000000000000..01ebb8c1ca05 --- /dev/null +++ b/tests/e2e/tests/mcp/projects-sourceroot-resolution.ts @@ -0,0 +1,47 @@ +import { exec, ProcessOutput, silentNpm } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import assert from 'node:assert/strict'; + +const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli'; +const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2'; +const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli'; + +async function runInspector(...args: string[]): Promise { + return exec(MCP_INSPECTOR_COMMAND_NAME, '--cli', 'npx', '--no', '@angular/cli', 'mcp', ...args); +} + +export default async function () { + await silentNpm( + 'install', + '--ignore-scripts', + '-g', + `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`, + ); + + try { + // 1. Add a sample project with a non-root path to angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + workspaceJson.projects ??= {}; + workspaceJson.projects['sample-lib'] = { + root: 'projects/sample-lib', + sourceRoot: 'projects/sample-lib/src', + projectType: 'library', + }; + }); + + // 2. Call list_projects + const { stdout } = await runInspector('--method', 'tools/call', '--tool-name', 'list_projects'); + + // 3. Verify output + assert.match(stdout, /"name": "sample-lib"/); + // Assert that sourceRoot is NOT duplicated + assert.match(stdout, /"sourceRoot": "projects\/sample-lib\/src"/); + assert.doesNotMatch(stdout, /"sourceRoot": "projects\/sample-lib\/projects\/sample-lib\/src"/); + } finally { + // 4. Cleanup angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + delete workspaceJson.projects['sample-lib']; + }); + await silentNpm('uninstall', '-g', MCP_INSPECTOR_PACKAGE_NAME); + } +} From 750c461a2ba448ebf449ef3b5d0222e3298a70e6 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Fri, 27 Mar 2026 06:01:34 +0000 Subject: [PATCH 06/11] build: update pnpm to v10.33.0 See associated pull request for more information. --- MODULE.bazel | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index df7172969df6..ec1986da8fb8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -111,8 +111,8 @@ use_repo( pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm") pnpm.pnpm( name = "pnpm", - pnpm_version = "10.32.1", - pnpm_version_integrity = "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==", + pnpm_version = "10.33.0", + pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==", ) use_repo(pnpm, "pnpm") diff --git a/package.json b/package.json index 1b7787da3f8a..4d7c78c3284a 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,12 @@ "type": "git", "url": "git+https://github.com/angular/angular-cli.git" }, - "packageManager": "pnpm@10.32.1", + "packageManager": "pnpm@10.33.0", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "Please use pnpm instead of NPM to install dependencies", "yarn": "Please use pnpm instead of Yarn to install dependencies", - "pnpm": "10.32.1" + "pnpm": "10.33.0" }, "author": "Angular Authors", "license": "MIT", From 57e956ce3b00111f9fcb8d05799e2cd8e2f37a6f Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Fri, 27 Mar 2026 09:49:44 +0000 Subject: [PATCH 07/11] build: update cross-repo angular dependencies See associated pull request for more information. --- .../assistant-to-the-branch-manager.yml | 2 +- .github/workflows/ci.yml | 52 +++++++++---------- .github/workflows/dev-infra.yml | 6 +-- .github/workflows/feature-requests.yml | 2 +- .github/workflows/perf.yml | 6 +-- .github/workflows/pr.yml | 44 ++++++++-------- MODULE.bazel | 4 +- packages/angular/build/package.json | 2 +- .../angular_devkit/build_angular/package.json | 2 +- pnpm-lock.yaml | 14 ++--- 10 files changed, 67 insertions(+), 67 deletions(-) diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml index c624b9aa858e..d32cd27e86d8 100644 --- a/.github/workflows/assistant-to-the-branch-manager.yml +++ b/.github/workflows/assistant-to-the-branch-manager.yml @@ -17,6 +17,6 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: angular/dev-infra/github-actions/branch-manager@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + - uses: angular/dev-infra/github-actions/branch-manager@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165b143eb20d..9235a0e18f71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Generate JSON schema types @@ -44,11 +44,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -61,11 +61,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -84,13 +84,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -100,11 +100,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -137,7 +137,7 @@ jobs: runs-on: windows-2025 steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Download built Windows E2E tests @@ -164,13 +164,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -188,13 +188,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -208,13 +208,13 @@ jobs: SAUCE_TUNNEL_IDENTIFIER: angular-cli-${{ github.workflow }}-${{ github.run_number }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run E2E Browser tests @@ -244,11 +244,11 @@ jobs: CIRCLE_BRANCH: ${{ github.ref_name }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - run: pnpm admin snapshots --verbose env: SNAPSHOT_BUILDS_GITHUB_TOKEN: ${{ secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN }} diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml index 9451bdbf330f..e2931fd90072 100644 --- a/.github/workflows/dev-infra.yml +++ b/.github/workflows/dev-infra.yml @@ -15,21 +15,21 @@ jobs: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/labeling/pull-request@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + - uses: angular/dev-infra/github-actions/labeling/pull-request@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} post_approval_changes: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/post-approval-changes@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + - uses: angular/dev-infra/github-actions/post-approval-changes@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} issue_labels: if: github.event_name == 'issues' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/labeling/issue@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + - uses: angular/dev-infra/github-actions/labeling/issue@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }} diff --git a/.github/workflows/feature-requests.yml b/.github/workflows/feature-requests.yml index 779a81c01f6a..a3c99b4b471b 100644 --- a/.github/workflows/feature-requests.yml +++ b/.github/workflows/feature-requests.yml @@ -16,6 +16,6 @@ jobs: if: github.repository == 'angular/angular-cli' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/feature-request@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + - uses: angular/dev-infra/github-actions/feature-request@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 7591c75d92b3..b1671b229d8a 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -23,7 +23,7 @@ jobs: workflows: ${{ steps.workflows.outputs.workflows }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - id: workflows @@ -38,9 +38,9 @@ jobs: workflow: ${{ fromJSON(needs.list.outputs.workflows) }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile # We utilize the google-github-actions/auth action to allow us to get an active credential using workflow diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 08af52b06b5c..11330223c837 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,9 +34,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup ESLint Caching uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: @@ -66,17 +66,17 @@ jobs: # it has been merged. run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }} - name: Check Package Licenses - uses: angular/dev-infra/github-actions/linting/licenses@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/linting/licenses@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc build: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Build release targets @@ -93,11 +93,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Run module and package tests @@ -114,13 +114,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Run CLI E2E tests run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests:e2e.${{ matrix.subset }}_node${{ matrix.node }} @@ -128,11 +128,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Build E2E tests for Windows on Linux @@ -156,7 +156,7 @@ jobs: runs-on: windows-2025 steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Download built Windows E2E tests @@ -183,13 +183,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Run CLI E2E tests run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=3 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests:e2e.${{ matrix.subset }}_node${{ matrix.node }} @@ -205,12 +205,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/setup@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@2f6d3ae5b1db37b5165f200fb53f30b9330983e4 + uses: angular/dev-infra/github-actions/bazel/configure-remote@55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc - name: Run CLI E2E tests run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} diff --git a/MODULE.bazel b/MODULE.bazel index ec1986da8fb8..7608ca41f96e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,14 +19,14 @@ bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4") bazel_dep(name = "rules_angular") git_override( module_name = "rules_angular", - commit = "af626f77ad610d1a9c47ee317af88e2c8edd66a4", + commit = "19a4a8fb4d6f035b5506ca21bbbd309ab5f5e729", remote = "https://github.com/angular/rules_angular.git", ) bazel_dep(name = "devinfra") git_override( module_name = "devinfra", - commit = "2f6d3ae5b1db37b5165f200fb53f30b9330983e4", + commit = "55af1f65dbeb0fe6a3de1d1134bd0dd8bc5e60cc", remote = "https://github.com/angular/dev-infra.git", ) diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index bca9e583729f..e04ef0c7b3a3 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -54,7 +54,7 @@ "@angular/ssr": "workspace:*", "jsdom": "28.1.0", "less": "4.4.2", - "ng-packagr": "21.2.1", + "ng-packagr": "21.2.2", "postcss": "8.5.6", "rxjs": "7.8.2", "vitest": "4.0.18" diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index ed47eb31a2bf..f4e7f207c2ec 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -68,7 +68,7 @@ "@angular/ssr": "workspace:*", "@web/test-runner": "0.20.2", "browser-sync": "3.0.4", - "ng-packagr": "21.2.1", + "ng-packagr": "21.2.2", "undici": "7.24.4" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a13dd4a5849..58b5e00312e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -432,8 +432,8 @@ importers: specifier: 4.4.2 version: 4.4.2 ng-packagr: - specifier: 21.2.1 - version: 21.2.1(@angular/compiler-cli@21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + specifier: 21.2.2 + version: 21.2.2(@angular/compiler-cli@21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) postcss: specifier: 8.5.6 version: 8.5.6 @@ -741,8 +741,8 @@ importers: specifier: 3.0.4 version: 3.0.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) ng-packagr: - specifier: 21.2.1 - version: 21.2.1(@angular/compiler-cli@21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) + specifier: 21.2.2 + version: 21.2.2(@angular/compiler-cli@21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) undici: specifier: 7.24.4 version: 7.24.4 @@ -7141,8 +7141,8 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - ng-packagr@21.2.1: - resolution: {integrity: sha512-rk0aL0wWkC+FTA4wyzJIfjcUgAKMAEB19ULvP0QkAoAkzjS+3SBEf0n3MS6z7gcOW8SRU9rw1BmsouEAXD+SCw==} + ng-packagr@21.2.2: + resolution: {integrity: sha512-VO0y7RU3Ik8E14QdrryVyVbTAyqO2MK9W9GrG4e/4N8+ti+DWiBSQmw0tIhnV67lEjQwCccPA3ZBoIn3B1vJ1Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: @@ -16629,7 +16629,7 @@ snapshots: netmask@2.0.2: {} - ng-packagr@21.2.1(@angular/compiler-cli@21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3): + ng-packagr@21.2.2(@angular/compiler-cli@21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3): dependencies: '@ampproject/remapping': 2.3.0 '@angular/compiler-cli': 21.2.6(@angular/compiler@21.2.6)(typescript@5.9.3) From 8f954e31c9bae01ceb28f6dcfe03cb94cfbfd311 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:00:27 -0400 Subject: [PATCH 08/11] build: safeguard terminal styling in devkit admin script Prior to this change, if an unhandled promise rejection or error occurred that lacked a `stack` property, the `console.error` wrapper in `devkit-admin.mts` would receive `undefined`. Attempting to pass `undefined` to Node`s `util.styleText` caused an unexpected `ERR_INVALID_ARG_TYPE` crash instead of printing the original error. This commit updates the `console.warn` and `console.error` overrides to ensure they only apply `styleText` to strings. It also updates the top-level try-catch block to fallback to the original error object if `err.stack` is undefined, preventing silent suppression. (cherry picked from commit 54c96c8dc02f090984e31f550e84c646829fe66b) --- scripts/devkit-admin.mts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/devkit-admin.mts b/scripts/devkit-admin.mts index 0a17df9f45a1..7d8522547100 100644 --- a/scripts/devkit-admin.mts +++ b/scripts/devkit-admin.mts @@ -33,12 +33,16 @@ process.chdir(path.join(scriptDir, '..')); const originalConsole = { ...console }; console.warn = function (...args) { - const [m, ...rest] = args; - originalConsole.warn(styleText(['yellow'], m), ...rest); + if (typeof args[0] === 'string') { + args[0] = styleText(['yellow'], args[0]); + } + originalConsole.warn(...args); }; console.error = function (...args) { - const [m, ...rest] = args; - originalConsole.error(styleText(['red'], m), ...rest); + if (typeof args[0] === 'string') { + args[0] = styleText(['red'], args[0]); + } + originalConsole.error(...args); }; try { @@ -47,6 +51,6 @@ try { process.exitCode = typeof exitCode === 'number' ? exitCode : 0; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { - console.error(err.stack); + console.error(err.stack ?? err); process.exitCode = 99; } From b7f4572533675729e87532bdc23509feb2f3a28d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:18:52 -0400 Subject: [PATCH 09/11] fix(@angular/build): scope CHROME_BIN executable path to individual playwright instances Previously, if CHROME_BIN was set in the environment and a user ran tests targeting the Playwright provider, the path was applied to the global Playwright launch options. This caused tests to crash if a user requested non-Chromium browsers (like Firefox) alongside Chromium, because Playwright would incorrectly attempt to launch the Chrome binary for the Firefox instance. This commit updates the browser configuration to map instances before providers are initialized, and selectively injects `launchOptions: { executablePath: process.env.CHROME_BIN }` at the individual instance level for chrome and chromium only. This restores parity where users can maintain CHROME_BIN variables while safely invoking alternative browsers. (cherry picked from commit 8dd341e21b8f44e8e2bf3f322cced8ff6e861098) --- .../runners/vitest/browser-provider.ts | 33 +++++--- .../runners/vitest/browser-provider_spec.ts | 76 +++++++++++++++++-- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts index 503f551c15cb..1ccbc1018aa9 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts @@ -37,7 +37,13 @@ function findBrowserProvider( return undefined; } -function normalizeBrowserName(browserName: string): { browser: string; headless: boolean } { +export interface BrowserInstanceConfiguration { + browser: string; + headless: boolean; + provider?: import('vitest/node').BrowserProviderOption; +} + +function normalizeBrowserName(browserName: string): BrowserInstanceConfiguration { // Normalize browser names to match Vitest's expectations for headless but also supports karma's names // e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' -> 'firefox' // and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'. @@ -79,6 +85,8 @@ export async function setupBrowserConfiguration( ); } + const instances = browsers.map(normalizeBrowserName); + let provider: import('vitest/node').BrowserProviderOption | undefined; if (providerName) { const providerPackage = `@vitest/browser-${providerName}`; @@ -90,17 +98,25 @@ export async function setupBrowserConfiguration( if (typeof providerFactory === 'function') { if (providerName === 'playwright') { const executablePath = process.env['CHROME_BIN']; - provider = providerFactory({ - launchOptions: executablePath - ? { - executablePath, - } - : undefined, + const baseOptions = { contextOptions: { // Enables `prefer-color-scheme` for Vitest browser instead of `light` colorScheme: null, }, - }); + }; + + provider = providerFactory(baseOptions); + + if (executablePath) { + for (const instance of instances) { + if (instance.browser === 'chrome' || instance.browser === 'chromium') { + instance.provider = providerFactory({ + ...baseOptions, + launchOptions: { executablePath }, + }); + } + } + } } else { provider = providerFactory(); } @@ -133,7 +149,6 @@ export async function setupBrowserConfiguration( } const isCI = !!process.env['CI']; - const instances = browsers.map(normalizeBrowserName); const messages: string[] = []; if (providerName === 'preview') { diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts index 66f7254593b0..f6b32d54a5a5 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts @@ -47,8 +47,8 @@ describe('setupBrowserConfiguration', () => { expect(browser?.enabled).toBeTrue(); expect(browser?.instances).toEqual([ - { browser: 'chrome', headless: true }, - { browser: 'firefox', headless: false }, + jasmine.objectContaining({ browser: 'chrome', headless: true }), + jasmine.objectContaining({ browser: 'firefox', headless: false }), ]); }); @@ -66,8 +66,8 @@ describe('setupBrowserConfiguration', () => { ); expect(browser?.instances).toEqual([ - { browser: 'chrome', headless: true }, - { browser: 'firefox', headless: true }, + jasmine.objectContaining({ browser: 'chrome', headless: true }), + jasmine.objectContaining({ browser: 'firefox', headless: true }), ]); } finally { if (originalCI === undefined) { @@ -196,8 +196,8 @@ describe('setupBrowserConfiguration', () => { ); expect(browser?.instances).toEqual([ - { browser: 'chrome', headless: true }, - { browser: 'firefox', headless: true }, + jasmine.objectContaining({ browser: 'chrome', headless: true }), + jasmine.objectContaining({ browser: 'firefox', headless: true }), ]); expect(messages).toEqual([]); }); @@ -215,4 +215,68 @@ describe('setupBrowserConfiguration', () => { 'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.', ]); }); + + describe('CHROME_BIN usage', () => { + let originalChromeBin: string | undefined; + + beforeEach(() => { + originalChromeBin = process.env['CHROME_BIN']; + process.env['CHROME_BIN'] = '/custom/path/to/chrome'; + }); + + afterEach(() => { + if (originalChromeBin === undefined) { + delete process.env['CHROME_BIN']; + } else { + process.env['CHROME_BIN'] = originalChromeBin; + } + }); + + it('should set executablePath on the individual chrome instance', async () => { + const { browser } = await setupBrowserConfiguration( + ['ChromeHeadless', 'Chromium'], + undefined, + false, + workspaceRoot, + undefined, + ); + + // Verify the global provider does NOT have executablePath + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((browser?.provider as any)?.options?.launchOptions?.executablePath).toBeUndefined(); + + // Verify the individual instances have executablePath + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (browser?.instances?.[0]?.provider as any)?.options?.launchOptions?.executablePath, + ).toBe('/custom/path/to/chrome'); + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (browser?.instances?.[1]?.provider as any)?.options?.launchOptions?.executablePath, + ).toBe('/custom/path/to/chrome'); + }); + + it('should set executablePath for chrome instances but not for others when mixed browsers are requested', async () => { + const { browser } = await setupBrowserConfiguration( + ['ChromeHeadless', 'Firefox'], + undefined, + false, + workspaceRoot, + undefined, + ); + + // Verify the global provider does NOT have executablePath + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((browser?.provider as any)?.options?.launchOptions?.executablePath).toBeUndefined(); + + // Verify chrome gets it + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (browser?.instances?.[0]?.provider as any)?.options?.launchOptions?.executablePath, + ).toBe('/custom/path/to/chrome'); + + // Verify firefox does not + expect(browser?.instances?.[1]?.provider).toBeUndefined(); + }); + }); }); From 7bfe34786887c26a49808d6cc105aca45fe522d0 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:59:50 -0400 Subject: [PATCH 10/11] refactor(@angular/build): extract headless configuration logic into helper function This extracts the verbose headless property mutation logic from the monolithic setupBrowserConfiguration function into a standalone applyHeadlessConfiguration helper function to improve maintainability and readability. (cherry picked from commit 3663f80f0738052cc569e87cfeafbdebbd8fc07a) --- .../runners/vitest/browser-provider.ts | 99 +++++++++++++------ .../runners/vitest/browser-provider_spec.ts | 40 +++++++- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts index 1ccbc1018aa9..0ca80f4fa60f 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts @@ -7,7 +7,11 @@ */ import { createRequire } from 'node:module'; -import type { BrowserBuiltinProvider, BrowserConfigOptions } from 'vitest/node'; +import type { + BrowserBuiltinProvider, + BrowserConfigOptions, + BrowserProviderOption, +} from 'vitest/node'; import { assertIsError } from '../../../../utils/error'; export interface BrowserConfiguration { @@ -40,7 +44,7 @@ function findBrowserProvider( export interface BrowserInstanceConfiguration { browser: string; headless: boolean; - provider?: import('vitest/node').BrowserProviderOption; + provider?: BrowserProviderOption; } function normalizeBrowserName(browserName: string): BrowserInstanceConfiguration { @@ -56,6 +60,67 @@ function normalizeBrowserName(browserName: string): BrowserInstanceConfiguration }; } +/** + * Mutates the provided browser instances to apply standard headless execution + * constraints based on the chosen provider, user options, and CI environment presence. + * + * @param instances The normalized browser instances to mutate. + * @param providerName The identifier for the chosen Vitest browser provider. + * @param headless The user-provided headless configuration option. + * @param isCI Whether the current environment is running in CI. + * @returns An array of informational messages generated during evaluation. + */ +export function applyHeadlessConfiguration( + instances: BrowserInstanceConfiguration[], + providerName: BrowserBuiltinProvider | undefined, + headless: boolean | undefined, + isCI: boolean, +): string[] { + const messages: string[] = []; + + if (providerName === 'preview') { + instances.forEach((instance) => { + // Preview mode only supports headed execution + instance.headless = false; + }); + + if (headless) { + messages.push('The "headless" option is ignored when using the "preview" provider.'); + } + } else if (headless !== undefined) { + if (headless) { + const allHeadlessByDefault = isCI || instances.every((i) => i.headless); + if (allHeadlessByDefault) { + messages.push( + 'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.', + ); + } + } + + instances.forEach((instance) => { + instance.headless = headless; + }); + } else if (isCI) { + instances.forEach((instance) => { + instance.headless = true; + }); + } + + return messages; +} + +/** + * Resolves and configures the Vitest browser provider for the unit test builder. + * Dynamically discovers and imports the necessary provider (Playwright, WebdriverIO, or Preview), + * maps the requested browser instances, and applies environment-specific execution logic. + * + * @param browsers An array of requested browser names (e.g., 'chrome', 'firefox'). + * @param headless User-provided configuration for headless execution. + * @param debug Whether the builder is running in watch or debug mode. + * @param projectSourceRoot The root directory of the project being tested for resolving installed packages. + * @param viewport Optional viewport dimensions to apply to the launched browser instances. + * @returns A fully resolved Vitest browser configuration object alongside any generated warning or error messages. + */ export async function setupBrowserConfiguration( browsers: string[] | undefined, headless: boolean | undefined, @@ -149,35 +214,7 @@ export async function setupBrowserConfiguration( } const isCI = !!process.env['CI']; - const messages: string[] = []; - - if (providerName === 'preview') { - instances.forEach((instance) => { - // Preview mode only supports headed execution - instance.headless = false; - }); - - if (headless) { - messages.push('The "headless" option is ignored when using the "preview" provider.'); - } - } else if (headless !== undefined) { - if (headless) { - const allHeadlessByDefault = isCI || instances.every((i) => i.headless); - if (allHeadlessByDefault) { - messages.push( - 'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.', - ); - } - } - - instances.forEach((instance) => { - instance.headless = headless; - }); - } else if (isCI) { - instances.forEach((instance) => { - instance.headless = true; - }); - } + const messages = applyHeadlessConfiguration(instances, providerName, headless, isCI); const browser = { enabled: true, diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts index f6b32d54a5a5..0dd0778420bd 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts @@ -9,7 +9,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { setupBrowserConfiguration } from './browser-provider'; +import { applyHeadlessConfiguration, setupBrowserConfiguration } from './browser-provider'; describe('setupBrowserConfiguration', () => { let workspaceRoot: string; @@ -279,4 +279,42 @@ describe('setupBrowserConfiguration', () => { expect(browser?.instances?.[1]?.provider).toBeUndefined(); }); }); + + describe('applyHeadlessConfiguration', () => { + it('should set headless false and issue warning when using preview provider with headless true', () => { + const instances = [{ browser: 'chrome', headless: true }]; + const messages = applyHeadlessConfiguration(instances, 'preview', true, false); + + expect(instances[0].headless).toBeFalse(); + expect(messages).toEqual([ + 'The "headless" option is ignored when using the "preview" provider.', + ]); + }); + + it('should force headless mode when headless option is true', () => { + const instances = [{ browser: 'chrome', headless: false }]; + const messages = applyHeadlessConfiguration(instances, 'playwright', true, false); + + expect(instances[0].headless).toBeTrue(); + expect(messages).toEqual([]); + }); + + it('should return information message when headless option is redundant', () => { + const instances = [{ browser: 'chrome', headless: true }]; + const messages = applyHeadlessConfiguration(instances, 'playwright', true, false); + + expect(instances[0].headless).toBeTrue(); + expect(messages).toEqual([ + 'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.', + ]); + }); + + it('should force headless mode in CI environment when headless is undefined', () => { + const instances = [{ browser: 'chrome', headless: false }]; + const messages = applyHeadlessConfiguration(instances, 'playwright', undefined, true); + + expect(instances[0].headless).toBeTrue(); + expect(messages).toEqual([]); + }); + }); }); From 9a2ee5190bd2fa1cecea23028c49cadf09d90840 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:06:53 +0000 Subject: [PATCH 11/11] release: cut the v21.2.6 release --- CHANGELOG.md | 21 +++++++++++++++++++++ package.json | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd79d6908e1..a6e5f434a182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + + +# 21.2.6 (2026-04-01) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [ea14f28cc](https://github.com/angular/angular-cli/commit/ea14f28ccfc6e5534eaef516bf1bfbe21582da04) | fix | fix sourceRoot resolution for MCP projects tool | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [9136eb376](https://github.com/angular/angular-cli/commit/9136eb37630d6315891b3c881cd0ba4037c3254c) | fix | ensure transitive SCSS partial errors are tracked in watch mode | +| [8186faa11](https://github.com/angular/angular-cli/commit/8186faa117803ffb6ac8e2c4cd6ab7873502308d) | fix | ensure Vitest mock patching is executed only once | +| [107d1a9e2](https://github.com/angular/angular-cli/commit/107d1a9e26fc59c7878254e563758818866f0f6e) | fix | preserve error stack traces during prerendering | +| [b7f457253](https://github.com/angular/angular-cli/commit/b7f4572533675729e87532bdc23509feb2f3a28d) | fix | scope CHROME_BIN executable path to individual playwright instances | + + + # 21.2.5 (2026-03-27) diff --git a/package.json b/package.json index 4d7c78c3284a..4ff49b3b7db4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "21.2.5", + "version": "21.2.6", "private": true, "description": "Software Development Kit for Angular", "keywords": [