diff --git a/.github/codeql/config.yml b/.github/codeql/config.yml
deleted file mode 100644
index ad81a268eda4..000000000000
--- a/.github/codeql/config.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-name: 'Angular CLI CodeQL config'
-
-query-filters:
- # TODO(josephperrott): reevaluate if these can be reenabled.
- - exclude:
- id: js/bad-code-sanitization
- - exclude:
- id: js/regex-injection
diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml
index 4a9daab0a148..9147cb3a65b8 100644
--- a/.github/workflows/assistant-to-the-branch-manager.yml
+++ b/.github/workflows/assistant-to-the-branch-manager.yml
@@ -18,6 +18,6 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- - uses: angular/dev-infra/github-actions/branch-manager@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ - uses: angular/dev-infra/github-actions/branch-manager@e713eca55b2813448af42f2e0e752258e4edfeea # main
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 05fa1692e7c8..b7d66e4aafc5 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Start Sauce Connect
@@ -245,11 +245,11 @@ jobs:
CIRCLE_BRANCH: ${{ github.ref_name }}
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- run: pnpm admin snapshots --verbose
env:
SNAPSHOT_BUILDS_GITHUB_TOKEN: ${{ secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index 7a75337fd3df..000000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: 'CodeQL'
-
-on:
- push:
- branches: ['main', '*.*.x']
- schedule:
- - cron: '39 9 * * 1'
-
-permissions: {}
-
-jobs:
- analyze:
- name: Analyze
- runs-on: 'ubuntu-latest'
- permissions:
- security-events: write
- packages: read
- strategy:
- fail-fast: false
- steps:
- - name: Checkout repository
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- persist-credentials: false
- - name: Initialize CodeQL
- uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
- with:
- languages: javascript-typescript
- build-mode: none
- config-file: .github/codeql/config.yml
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
- with:
- category: '/language:javascript-typescript'
diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml
index 0be73a55a51a..f0b468e675ea 100644
--- a/.github/workflows/dev-infra.yml
+++ b/.github/workflows/dev-infra.yml
@@ -16,21 +16,21 @@ jobs:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- - uses: angular/dev-infra/github-actions/labeling/pull-request@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ - uses: angular/dev-infra/github-actions/labeling/pull-request@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ - uses: angular/dev-infra/github-actions/post-approval-changes@e713eca55b2813448af42f2e0e752258e4edfeea # main
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ - uses: angular/dev-infra/github-actions/labeling/issue@e713eca55b2813448af42f2e0e752258e4edfeea # main
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml
index 5bca0fe665c8..392fbd4cc108 100644
--- a/.github/workflows/perf.yml
+++ b/.github/workflows/perf.yml
@@ -22,7 +22,7 @@ jobs:
workflows: ${{ steps.workflows.outputs.workflows }}
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- id: workflows
@@ -40,9 +40,9 @@ jobs:
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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 4100bed1bcc8..0837c2b0fa67 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
+ - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
filters: |
@@ -34,9 +34,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup ESLint Caching
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/linting/licenses@e713eca55b2813448af42f2e0e752258e4edfeea # main
build:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/setup@e713eca55b2813448af42f2e0e752258e4edfeea # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@0b701b31038419ad73422a9d9d3dff43a725a0d2 # main
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@e713eca55b2813448af42f2e0e752258e4edfeea # main
- 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/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
deleted file mode 100644
index ac5d75573754..000000000000
--- a/.github/workflows/scorecard.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-name: OpenSSF Scorecard
-on:
- branch_protection_rule:
- schedule:
- - cron: '0 2 * * 0'
- push:
- branches: [main]
- workflow_dispatch:
-
-# Declare default permissions as read only.
-permissions:
- contents: read
-
-jobs:
- analysis:
- name: Scorecards analysis
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- # Needed to upload the results to code-scanning dashboard.
- security-events: write
- # Needed to publish results
- id-token: write
-
- steps:
- - name: 'Checkout code'
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- persist-credentials: false
-
- - name: 'Run analysis'
- uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
- with:
- results_file: results.sarif
- results_format: sarif
- publish_results: true
-
- # Upload the results as artifacts.
- - name: 'Upload artifact'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- with:
- name: SARIF file
- path: results.sarif
- retention-days: 5
-
- # Upload the results to GitHub's code scanning dashboard.
- - name: 'Upload to code-scanning'
- uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
- with:
- sarif_file: results.sarif
diff --git a/.nvmrc b/.nvmrc
index c94711948a66..60ade1ae01e8 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.23.2
+24.19.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ff82d9e52d9..e1fcb5a988ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,258 @@
+
+
+# 22.2.0-next.5 (2026-08-26)
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------- |
+| [4d0be683b7](https://github.com/angular/angular-cli/commit/4d0be683b780a9a1629dc8f9cddad49153bddabf) | fix | ignore side-effect annotations in unit test builds |
+| [e4111ae818](https://github.com/angular/angular-cli/commit/e4111ae8180f3cd5800f248c23010844aa1730ad) | fix | prevent memory leaks in parallel compilation and caches |
+| [62999cabf4](https://github.com/angular/angular-cli/commit/62999cabf4d65cf1060598150a77494e147fcc2a) | perf | batch prerender routes and share in-memory server bundles |
+| [0589edd9cd](https://github.com/angular/angular-cli/commit/0589edd9cdc493ecf34a160cc7b8491618b59f72) | perf | consolidate component stylesheet bundling with shared load result cache |
+| [a877aa7ebd](https://github.com/angular/angular-cli/commit/a877aa7ebdbf919d6bc17e6ec0920ca702db0db4) | perf | implement SharedArrayBuffer translation dictionaries for zero-copy worker access |
+| [f1cc959439](https://github.com/angular/angular-cli/commit/f1cc959439d8ca11a45d693520c34dd94e0b6548) | perf | implement sliding-window batching and worker translation eviction |
+| [f4dad31cce](https://github.com/angular/angular-cli/commit/f4dad31cce41bc3e8ed8767256d9e50a712c3a72) | perf | unify Oxc linking and optimization AST traversal passes |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- |
+| [f8576e3c8b](https://github.com/angular/angular-cli/commit/f8576e3c8bfe3490b1b0b85832b489593a4e43ac) | fix | abort web request signal when node request is aborted |
+
+
+
+
+
+# 22.1.6 (2026-08-26)
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- |
+| [3f132eaa97](https://github.com/angular/angular-cli/commit/3f132eaa97b95e16c58b8095c2566e3c78190446) | fix | ignore side-effect annotations in unit test builds |
+| [5b1028936f](https://github.com/angular/angular-cli/commit/5b1028936f6e9582917f95ac37942dca0ee2c659) | perf | batch prerender routes and share in-memory server bundles |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- |
+| [76818cb811](https://github.com/angular/angular-cli/commit/76818cb811a8fe669575f988fec2e4528dca8c73) | fix | abort web request signal when node request is aborted |
+
+
+
+
+
+# 21.2.22 (2026-08-26)
+
+### @angular-devkit/build-angular
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- |
+| [b0047b04f5](https://github.com/angular/angular-cli/commit/b0047b04f56f5cb3fb8e4465ba0fad777497144a) | fix | update webpack-dev-server to 5.2.6 |
+
+
+
+
+
+# 20.3.35 (2026-08-26)
+
+### @angular-devkit/build-angular
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- |
+| [94b620acd7](https://github.com/angular/angular-cli/commit/94b620acd76e39c7d5cc8527dfc6fe9096855cbd) | fix | update webpack-dev-server to 5.2.6 |
+
+
+
+
+
+# 22.2.0-next.4 (2026-08-19)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- |
+| [34e1e0bb5](https://github.com/angular/angular-cli/commit/34e1e0bb5d5ccf374f51a1b241158f6f2ea0dd42) | fix | enforce MCP roots in get_best_practices tool |
+| [2b060630c](https://github.com/angular/angular-cli/commit/2b060630c66ce688b7ecbe51d282955869e5d4b0) | fix | handle errors from isAllowedWorkspacePath in best-practices tool |
+| [24fd8fce2](https://github.com/angular/angular-cli/commit/24fd8fce2a3ff035acdc5f3e036eb4ba21f71a76) | fix | throw on out-of-roots workspace in best-practices tool |
+
+### @schematics/angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ |
+| [ce1b60f89](https://github.com/angular/angular-cli/commit/ce1b60f89699c3a76496a0489e5f2b9e4fe62429) | fix | transform fail() to expect.fail() in refactor-jasmine-vitest |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------- |
+| [175273931](https://github.com/angular/angular-cli/commit/175273931ebb0cf08bf62dd1694dbde5ff5229b6) | feat | Support splitting browser and server stats jsonfiles for easier consumption |
+| [1ee0beca7](https://github.com/angular/angular-cli/commit/1ee0beca7b243cc4c06eb42b25d216d7b256b097) | fix | correct misleading error message for top-level await |
+| [0ffe2d27f](https://github.com/angular/angular-cli/commit/0ffe2d27f256a5755809fa4788e4adb97c65b806) | fix | disable code splitting for unit test builds |
+| [50994d76e](https://github.com/angular/angular-cli/commit/50994d76e59a4b632f1782c51cd60f80c9cd50fb) | fix | preserve integrity and crossorigin in autoCsp loader |
+| [d6fd24320](https://github.com/angular/angular-cli/commit/d6fd243207ad1c3e242b8592698afa986320cc6c) | perf | traverse AST with iterative post-order walker in i18n inliner |
+
+
+
+
+
+# 22.1.5 (2026-08-19)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- |
+| [e672271f8](https://github.com/angular/angular-cli/commit/e672271f81e7b8b422b98c17fca5b67896d40844) | fix | enforce MCP roots in get_best_practices tool |
+| [a14916cc4](https://github.com/angular/angular-cli/commit/a14916cc4b13412205d3e4a1c32d7e40be52f0e1) | fix | handle errors from isAllowedWorkspacePath in best-practices tool |
+| [d6e1cddff](https://github.com/angular/angular-cli/commit/d6e1cddff92842f6889585ae7899efc028decc78) | fix | throw on out-of-roots workspace in best-practices tool |
+
+### @schematics/angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ |
+| [ba2b0e4c2](https://github.com/angular/angular-cli/commit/ba2b0e4c291842c699fdbeaaddec1531dbdd7952) | fix | transform fail() to expect.fail() in refactor-jasmine-vitest |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- |
+| [cba72902d](https://github.com/angular/angular-cli/commit/cba72902d42022bd5a1bd17b23d65cacc0013e35) | fix | correct misleading error message for top-level await |
+| [b4679998c](https://github.com/angular/angular-cli/commit/b4679998c205b6cce01d40fad24c4ee7954aa48d) | fix | disable code splitting for unit test builds |
+| [ff1d3565e](https://github.com/angular/angular-cli/commit/ff1d3565e87749a8347c3be98cd4a7ebccb69fc0) | fix | preserve integrity and crossorigin in autoCsp loader |
+| [1fc1fb05c](https://github.com/angular/angular-cli/commit/1fc1fb05cfe2c69ee7b9345b582d3c2177b923fd) | perf | traverse AST with iterative post-order walker in i18n inliner |
+
+
+
+
+
+# 22.2.0-next.3 (2026-08-13)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| [7106676e6](https://github.com/angular/angular-cli/commit/7106676e64ee234641b1b3cbd88fc058703d4d3e) | fix | disable searching current directory for bare executable names on Windows |
+| [ecf8c0822](https://github.com/angular/angular-cli/commit/ecf8c0822e5d56b44a0be4be8fccb41373135d3e) | fix | serialize configuration as a single argv token in run_target strategies ([#33657](https://github.com/angular/angular-cli/pull/33657)) |
+| [2e4dd90c3](https://github.com/angular/angular-cli/commit/2e4dd90c3ae898bf36af42f699c7b7d560bfc66a) | perf | avoid eager module loading during global bootstrap |
+
+### @schematics/angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- |
+| [1161e6c99](https://github.com/angular/angular-cli/commit/1161e6c99992c884e2e0ce9fc295b0e60b2ae1df) | fix | generate CLAUDE.md for Claude Code instead of AGENTS.md |
+| [c536ae364](https://github.com/angular/angular-cli/commit/c536ae364975dd0088fd8717464c723a4dcb74e3) | fix | import UrlSegment instead of subPath in guard generator |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- |
+| [04b532772](https://github.com/angular/angular-cli/commit/04b532772f6347d341b182135dee1758616a69d6) | fix | count statically imported chunks in the initial total |
+| [2274babb6](https://github.com/angular/angular-cli/commit/2274babb662b8da2189f1ae9faaa528838052c32) | fix | normalize setupFiles paths to POSIX for vitest runner |
+| [199a864df](https://github.com/angular/angular-cli/commit/199a864df97fc8c20b8d4e1fc8b891c61140671a) | fix | prevent syntax corruption for Crockford-style enum IIFE |
+| [0d9851600](https://github.com/angular/angular-cli/commit/0d9851600ca618ccabfc3143a9c552a5448d6eec) | fix | recursively ignore output and cache paths in watch mode |
+| [3eba6f726](https://github.com/angular/angular-cli/commit/3eba6f7269f3b0f8556510d0b668e978ea686c88) | fix | return direct file contents for non-Angular TypeScript files |
+| [3c7ac1518](https://github.com/angular/angular-cli/commit/3c7ac1518b062f14409b4d3c6c3e65c956424ad9) | fix | return only lowest version per target engine |
+| [3e9fed9a6](https://github.com/angular/angular-cli/commit/3e9fed9a6a1d24f30812ca29ca621d9d9a14496f) | fix | set target for Rolldown dependency prebundling in Vite dev server |
+| [87551ad5c](https://github.com/angular/angular-cli/commit/87551ad5c53f90cbdc68fdb5e47007b9436c7f75) | perf | avoid encoding intermediate source maps before remapping |
+| [194be2088](https://github.com/angular/angular-cli/commit/194be2088587918ed7b47ed606ea587a7116608a) | perf | avoid encoding the inline source map before remapping |
+| [a55a6b78e](https://github.com/angular/angular-cli/commit/a55a6b78e89debb9c93c83375c54f6716bc26c65) | perf | batch last_accessed updates in sqlite cache store |
+| [33b305416](https://github.com/angular/angular-cli/commit/33b3054169929dc98f93a758a90da5d26cf522f7) | perf | hash the i18n inline cache key options once per locale |
+| [1c00edce0](https://github.com/angular/angular-cli/commit/1c00edce02bdd0201546417d71518f78bfb89d34) | perf | optimize sourcemap stripping and loading with buffer fast path |
+| [b6269a816](https://github.com/angular/angular-cli/commit/b6269a8169046a78281808dcf92054140912a77d) | perf | optimize template string size calculation in server manifest |
+| [a6ef9cfbe](https://github.com/angular/angular-cli/commit/a6ef9cfbeace725d58c0f7f65640ef6de9b39c33) | perf | replace watchpack with @parcel/watcher and chokidar |
+| [596847f89](https://github.com/angular/angular-cli/commit/596847f89913684a84465d004aa3d6c7e1a40b06) | perf | share i18n translations with the inliner workers by reference |
+| [19c91e48d](https://github.com/angular/angular-cli/commit/19c91e48d4b8a95a76dfe4bffd105130b416843d) | perf | use Map for chunk asset size lookups in budget calculator |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- |
+| [6a3446770](https://github.com/angular/angular-cli/commit/6a3446770a8fbfecc7265eb2a6d764e2f1614b3a) | fix | destroy platform when response stream is cancelled |
+
+
+
+
+
+# 22.1.4 (2026-08-13)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| [67a29899b](https://github.com/angular/angular-cli/commit/67a29899b07f760bac9582cbc6175966cce1c5d2) | fix | disable searching current directory for bare executable names on Windows |
+| [1b0ba5c17](https://github.com/angular/angular-cli/commit/1b0ba5c17f0be2679b31bad1bee8b435315dac1a) | fix | serialize configuration as a single argv token in run_target strategies ([#33657](https://github.com/angular/angular-cli/pull/33657)) |
+| [9440432d2](https://github.com/angular/angular-cli/commit/9440432d273c044965508d80e808c2fe123af0aa) | perf | avoid eager module loading during global bootstrap |
+
+### @schematics/angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- |
+| [d4a48e3ab](https://github.com/angular/angular-cli/commit/d4a48e3abe320e090eefe17cb70abdf8e7eafaed) | fix | generate CLAUDE.md for Claude Code instead of AGENTS.md |
+| [d451f15b2](https://github.com/angular/angular-cli/commit/d451f15b2838a8282363dcb30161dd5dc858750f) | fix | import UrlSegment instead of subPath in guard generator |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- |
+| [46fcb29d6](https://github.com/angular/angular-cli/commit/46fcb29d6c1ded962de95449d63716c13950f59a) | fix | count statically imported chunks in the initial total |
+| [796b57684](https://github.com/angular/angular-cli/commit/796b576843191a4d5c74e4f9363ec0e46ae3a881) | fix | normalize setupFiles paths to POSIX for vitest runner |
+| [c805e5cfb](https://github.com/angular/angular-cli/commit/c805e5cfb308d02a1944ef446d0a01af8a7ec6d2) | fix | prevent syntax corruption for Crockford-style enum IIFE |
+| [cc6c17716](https://github.com/angular/angular-cli/commit/cc6c17716df5c411efca5e6f4c960f0565f45d90) | fix | prevent syntax corruption in oxc transform |
+| [a142f6f83](https://github.com/angular/angular-cli/commit/a142f6f83d2f501b64f2404909ef9325bbfa70fb) | fix | return direct file contents for non-Angular TypeScript files |
+| [956c6578d](https://github.com/angular/angular-cli/commit/956c6578d31d8482c04dc45723d7ab3b4fe355a5) | fix | return only lowest version per target engine |
+| [689b5b116](https://github.com/angular/angular-cli/commit/689b5b116717140da98494e61ac5ea5ad7b68d73) | fix | set target for Rolldown dependency prebundling in Vite dev server |
+| [70461f6ed](https://github.com/angular/angular-cli/commit/70461f6ed2e30aeaad2b93400ad00d9274665967) | perf | avoid encoding the inline source map before remapping |
+| [22cce7563](https://github.com/angular/angular-cli/commit/22cce7563e82f17373b69e590d3e445767bd9264) | perf | batch last_accessed updates in sqlite cache store |
+| [ab2ed18d5](https://github.com/angular/angular-cli/commit/ab2ed18d5739e8565403062ca74aa2d5cb3b95ec) | perf | hash the i18n inline cache key options once per locale |
+| [34516ff73](https://github.com/angular/angular-cli/commit/34516ff736185e20ef8239b1fed8eda1f0193f62) | perf | optimize template string size calculation in server manifest |
+| [712971c37](https://github.com/angular/angular-cli/commit/712971c370738beaebff18f6995859b1713ed8ae) | perf | share i18n translations with the inliner workers by reference |
+| [78f8c5fef](https://github.com/angular/angular-cli/commit/78f8c5fef44dc6ff466b17c98fa1d01222cb1781) | perf | use Map for chunk asset size lookups in budget calculator |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- |
+| [2331c1047](https://github.com/angular/angular-cli/commit/2331c10476830e7a53e0f2fa4a2f37dad58e2d5d) | fix | destroy platform when response stream is cancelled |
+
+
+
+
+
+# 21.2.21 (2026-08-12)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ |
+| [ba5d16cac](https://github.com/angular/angular-cli/commit/ba5d16cacab4139897d1f0cf4161562b1bd00553) | fix | update dependency @modelcontextprotocol/sdk to v1.30.0 |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | --------------------- |
+| [f7f60e69e](https://github.com/angular/angular-cli/commit/f7f60e69e77cda60378543a9ca9cc2c76c0bf34f) | fix | bump undici to 7.29.0 |
+
+
+
+
+
+# 20.3.34 (2026-08-12)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ |
+| [3962517b9](https://github.com/angular/angular-cli/commit/3962517b95c76016ff7bcb53662704dff18c0552) | fix | update dependency @modelcontextprotocol/sdk to v1.30.0 |
+
+### @angular-devkit/build-angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | --------------------- |
+| [f348efe8b](https://github.com/angular/angular-cli/commit/f348efe8b6d9c9d7014afb331eecda0a9478ba0a) | fix | bump undici to 7.29.0 |
+
+
+
# 22.2.0-next.2 (2026-08-05)
diff --git a/MODULE.bazel b/MODULE.bazel
index d4fb6902ff2a..3015308e9d38 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -7,48 +7,48 @@ module(
bazel_dep(name = "platforms", version = "1.1.0")
bazel_dep(name = "yq.bzl", version = "0.3.6")
bazel_dep(name = "rules_nodejs", version = "6.7.5")
-bazel_dep(name = "aspect_rules_js", version = "3.3.1")
-bazel_dep(name = "aspect_rules_ts", version = "3.9.2")
-bazel_dep(name = "rules_pkg", version = "1.2.0")
+bazel_dep(name = "aspect_rules_js", version = "3.4.0")
+bazel_dep(name = "aspect_rules_ts", version = "3.10.0")
+bazel_dep(name = "rules_pkg", version = "1.3.0")
bazel_dep(name = "rules_cc", version = "0.2.22")
bazel_dep(name = "jq.bzl", version = "0.6.1")
-bazel_dep(name = "bazel_lib", version = "3.5.0")
+bazel_dep(name = "bazel_lib", version = "3.7.1")
bazel_dep(name = "bazel_skylib", version = "1.9.2")
-bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0")
+bazel_dep(name = "aspect_rules_esbuild", version = "0.27.0")
bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4")
bazel_dep(name = "rules_angular")
git_override(
module_name = "rules_angular",
- commit = "a0bbcceea19b7888114eeafe3f52f9822ca2b34a",
+ commit = "a30fc96ae7cbf6738d37d9d7d3e1c6e6cb7744f5",
remote = "https://github.com/angular/rules_angular.git",
)
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
- commit = "0b701b31038419ad73422a9d9d3dff43a725a0d2",
+ commit = "e713eca55b2813448af42f2e0e752258e4edfeea",
remote = "https://github.com/angular/dev-infra.git",
)
bazel_dep(name = "rules_browsers")
git_override(
module_name = "rules_browsers",
- commit = "7fe4598226334b0809d3054f6948d72df4b5ed59",
+ commit = "c6cb5590120af8ae8125343f9fc522b7b66eb0e8",
remote = "https://github.com/angular/rules_browsers.git",
)
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(
node_repositories = {
- "22.23.2-darwin_arm64": ("node-v22.23.2-darwin-arm64.tar.gz", "node-v22.23.2-darwin-arm64", "61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6"),
- "22.23.2-darwin_amd64": ("node-v22.23.2-darwin-x64.tar.gz", "node-v22.23.2-darwin-x64", "58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026"),
- "22.23.2-linux_arm64": ("node-v22.23.2-linux-arm64.tar.xz", "node-v22.23.2-linux-arm64", "fff4078c5def658577f92c88db7db3bc0072924bfb93fe52c1e744a54e94abb8"),
- "22.23.2-linux_ppc64le": ("node-v22.23.2-linux-ppc64le.tar.xz", "node-v22.23.2-linux-ppc64le", "e2ba10110ad34096c051fe47134fcd12175ecc48e7e879d7d4c78b70cf66442c"),
- "22.23.2-linux_s390x": ("node-v22.23.2-linux-s390x.tar.xz", "node-v22.23.2-linux-s390x", "50aa0935c7caee2f95434f84c2c19f16e4f223257eadb341a3a2d5aaa545bbe6"),
- "22.23.2-linux_amd64": ("node-v22.23.2-linux-x64.tar.xz", "node-v22.23.2-linux-x64", "d60acfe00a2932254bb0ad20e01b0d74397a0875595de719654b214f4b03f307"),
- "22.23.2-windows_amd64": ("node-v22.23.2-win-x64.zip", "node-v22.23.2-win-x64", "1177b4137ba5adaa56354ae40f1080c7450e8ae09cecb47da459d1c52ac99f97"),
+ "24.19.0-darwin_arm64": ("node-v24.19.0-darwin-arm64.tar.gz", "node-v24.19.0-darwin-arm64", "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d"),
+ "24.19.0-darwin_amd64": ("node-v24.19.0-darwin-x64.tar.gz", "node-v24.19.0-darwin-x64", "d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316"),
+ "24.19.0-linux_arm64": ("node-v24.19.0-linux-arm64.tar.xz", "node-v24.19.0-linux-arm64", "01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc"),
+ "24.19.0-linux_ppc64le": ("node-v24.19.0-linux-ppc64le.tar.xz", "node-v24.19.0-linux-ppc64le", "c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3"),
+ "24.19.0-linux_s390x": ("node-v24.19.0-linux-s390x.tar.xz", "node-v24.19.0-linux-s390x", "a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4"),
+ "24.19.0-linux_amd64": ("node-v24.19.0-linux-x64.tar.xz", "node-v24.19.0-linux-x64", "14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647"),
+ "24.19.0-windows_amd64": ("node-v24.19.0-win-x64.zip", "node-v24.19.0-win-x64", "57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73"),
},
- node_version = "22.23.2",
+ node_version = "24.19.0",
)
use_repo(
node,
@@ -131,8 +131,8 @@ use_repo(
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
pnpm.pnpm(
name = "pnpm",
- pnpm_version = "11.20.0",
- pnpm_version_integrity = "sha512-mm8zCpW2ZEbqCI+vFSFAWooB8H/ecSTMmVjf7VLUu0NnN+ZbCPhfN7Rvy6N1CSVYrFEmK4FoRLIvY0Bu0Wa/7g==",
+ pnpm_version = "11.24.0",
+ pnpm_version_integrity = "sha512-vSfjRel23LC+C3oSKCF7BJqBfiGx81XJDb59xGZxiVqLwebQbCRVRQXqk+oLRfSJon7Bv7yN5qlln8oPFvoAAA==",
)
use_repo(pnpm, "pnpm")
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index a30e6d148916..ee16930eae77 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -18,16 +18,15 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
- "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.26.0/MODULE.bazel": "6c902d97038c3ab07b6c4e67c97abc61b20182fcfa84fa7dee82fc724f12e455",
- "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.26.0/source.json": "4cc3ece7ab661bb391a9e24fe55c4b567d60a9ea9d9e91d772dad373cbcb6217",
+ "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.27.0/MODULE.bazel": "877dafc0b925f8af19e8bc2abed04a757bb565c57c1866e8851ac4d15ed5e6d2",
+ "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.27.0/source.json": "21f8738b3e62310ef43b7cef4284e1bafd69bd8e4e50251b71b20bbfed4372d8",
"https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.4/MODULE.bazel": "fbb819eb8b7e5d7f67fdd38f7cecb413e287594cd666ce192c72c8828527775a",
"https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.4/source.json": "81ffb708333cd98ec3c0b4cc004f4d5cf92a16914b5196a2892c45141bba7cff",
"https://bcr.bazel.build/modules/aspect_rules_js/2.0.0/MODULE.bazel": "b45b507574aa60a92796e3e13c195cd5744b3b8aff516a9c0cb5ae6a048161c5",
- "https://bcr.bazel.build/modules/aspect_rules_js/3.0.3/MODULE.bazel": "28a30e8fc33bf64a67835d64d124f6e05a7d59648dcb27b110fb3502f761e503",
- "https://bcr.bazel.build/modules/aspect_rules_js/3.3.1/MODULE.bazel": "3e02b51b503ba8dda69b043290f6cc11add9aeb8db0bf1f6c861c396c7ddc5b2",
- "https://bcr.bazel.build/modules/aspect_rules_js/3.3.1/source.json": "d5e0736539b00bdd33015bb336877ad5a4d16f1d1d4af13ffeade395106cabd7",
- "https://bcr.bazel.build/modules/aspect_rules_ts/3.9.2/MODULE.bazel": "feeb6c45b69c995eca3e5ca5872658c80df658022e01044eca00cf472bb89142",
- "https://bcr.bazel.build/modules/aspect_rules_ts/3.9.2/source.json": "cf3075502f798f71a9c5707a7684eaf0b86da11ed440fd90ea34385dc297676a",
+ "https://bcr.bazel.build/modules/aspect_rules_js/3.4.0/MODULE.bazel": "88844ac411e1961f4574a92f3c5be5b20d1c6997778c6b88316c5c3b4b60e284",
+ "https://bcr.bazel.build/modules/aspect_rules_js/3.4.0/source.json": "85e5822f00dcbe64a1eda1324119e289c8c03cacb5c3695dffee16397b529078",
+ "https://bcr.bazel.build/modules/aspect_rules_ts/3.10.0/MODULE.bazel": "69d06f57f30f4a2b6e53471584a9559d3b7cd7f891e1699876991230c7cabb95",
+ "https://bcr.bazel.build/modules/aspect_rules_ts/3.10.0/source.json": "56f28a3ddb55ceaaf57a1ef8d7195136789ca1d72d0c8a6a9eeaad313be4099d",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/MODULE.bazel": "f31aa84151d31e98cffd43eb7217ccff5ec52bdd5f2d10db8f053aeb23342eca",
@@ -51,9 +50,9 @@
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
"https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18",
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
- "https://bcr.bazel.build/modules/bazel_lib/3.2.2/MODULE.bazel": "e2c890c8a515d6bca9c66d47718aa9e44b458fde64ec7204b8030bf2d349058c",
- "https://bcr.bazel.build/modules/bazel_lib/3.5.0/MODULE.bazel": "1eba0ba67d418d99fb682c36147a9e1725abbc8fc25544be64702e32eda2be29",
- "https://bcr.bazel.build/modules/bazel_lib/3.5.0/source.json": "98d49ec7e2b15298c990f09ec8cdea8d4788c2885497aa9cbb0a74e36e994df8",
+ "https://bcr.bazel.build/modules/bazel_lib/3.7.0/MODULE.bazel": "d7c10ed67f0f7f1fda179db8f86c22642581bd614882e1a50545fbe069525173",
+ "https://bcr.bazel.build/modules/bazel_lib/3.7.1/MODULE.bazel": "b6fd9b2f8fab956420c11836f416efac4a70e20804ae384ebe62773a4ed70046",
+ "https://bcr.bazel.build/modules/bazel_lib/3.7.1/source.json": "635fdaa28b50c04febc5e60ef51bc913d3bc87bfbaac7045449273c2341648cb",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
@@ -68,6 +67,7 @@
"https://bcr.bazel.build/modules/bazel_skylib/1.8.0/MODULE.bazel": "2fb3fb53675f6adfc1ca5bfbd5cfb655ae350fba4706d924a8ec7e3ba945671c",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7",
"https://bcr.bazel.build/modules/bazel_skylib/1.9.2/MODULE.bazel": "8c51259b0f4481475586dbfede7591e57b75702e637f840f6138eec80c34b270",
"https://bcr.bazel.build/modules/bazel_skylib/1.9.2/source.json": "41cbde7546542dee2f26e3f10bf1c4ac57909943194b7ac48cc5238a01893aa8",
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
@@ -170,8 +170,8 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.7.5/source.json": "d60ee5a76258b1c8f99545ed24172b44d43ba64ca1a2dfc04371ef203df19fdf",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
- "https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
- "https://bcr.bazel.build/modules/rules_pkg/1.2.0/source.json": "9062e00845bf91a4247465d371baa837adf9b6ff44c542f73ba084f07667e1dc",
+ "https://bcr.bazel.build/modules/rules_pkg/1.3.0/MODULE.bazel": "ae0bdefbacc990c91f843206c90cf0f4be620639a5bf22119043599ba86d51a3",
+ "https://bcr.bazel.build/modules/rules_pkg/1.3.0/source.json": "58ae84c545141762f7c434c0ce78bfb55ef0be08d84863e7f0503fff135fe4e2",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f",
@@ -185,7 +185,6 @@
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
- "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
"https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8",
"https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32",
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
@@ -201,14 +200,12 @@
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/tar.bzl/0.10.4/MODULE.bazel": "e8f9ff79199e8d9eaad7f1b0a77ad74b30bb82d794b87d8ca942bead5de83ae9",
- "https://bcr.bazel.build/modules/tar.bzl/0.10.7/MODULE.bazel": "e06d0072c8adef7b9efbeec951d7e6b4b7e6bfa5845c3d8f402289c7b5d6331c",
- "https://bcr.bazel.build/modules/tar.bzl/0.10.7/source.json": "c660155f239fcfadfb85f0b9ff304b95390632c15e3f6cb718cd3e08f2bb5c86",
+ "https://bcr.bazel.build/modules/tar.bzl/0.10.8/MODULE.bazel": "443884cabe241f640cfef256b1de2ccb116752895532900a4be9500e1124323f",
+ "https://bcr.bazel.build/modules/tar.bzl/0.10.8/source.json": "4173be64b38e471d92d2eb139a6496311de6de75e710f04edce78c43122c5419",
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
- "https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
- "https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/MODULE.bazel": "d3a270662f5d766cd7229732d65a5a5bc485240c3007343dd279edfb60c9ae27",
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/MODULE.bazel": "985c2a0cb4ad9994bb0e33cc7fae931c91105eeefe3faa355b8f4c258d0607c0",
"https://bcr.bazel.build/modules/yq.bzl/0.3.6/source.json": "678aaf6e291164f3cd761bb3e872e8a151248f413dbb63c5524a50b82a5bc890",
@@ -222,8 +219,8 @@
"moduleExtensions": {
"@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": {
"general": {
- "bzlTransitiveDigest": "Nhop9eqlQBcxge+uYzGmhiFAZmagl+D5CUXkaUpOBCc=",
- "usagesDigest": "LSQ+zZp7JNgnBONTxxXnwGr4NTh2qtQYk7qwXXz5qWo=",
+ "bzlTransitiveDigest": "eGsGosHZTHOEZx11j7nDLNMHxCkESoe1oypcL3+G6GI=",
+ "usagesDigest": "LZ71sshnfqI8R/HeWwBTwawxIa7KnitayBl2hYrqPo4=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -438,7 +435,7 @@
"@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
"general": {
"bzlTransitiveDigest": "4w9RM0xjdKo1crk5zL20a/TuhqO0P1z1LsuXDneBXD4=",
- "usagesDigest": "q7mSkkDF8zaV9mtEPYuvConVymuL69UDTnd+J5Pgo0w=",
+ "usagesDigest": "VoGyqZ+r/3juGZUfSXD4FAe965Z3XS4WANRrYH79+1Q=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {
@@ -449,9 +446,9 @@
"repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository",
"attributes": {
"deps": {
- "aspect_rules_js": "3.3.1",
- "aspect_rules_ts": "3.9.2",
- "aspect_rules_esbuild": "0.26.0",
+ "aspect_rules_js": "3.4.0",
+ "aspect_rules_ts": "3.10.0",
+ "aspect_rules_esbuild": "0.27.0",
"aspect_rules_jasmine": "2.0.4",
"aspect_tools_telemetry": "0.4.2"
},
@@ -539,7 +536,7 @@
},
"@@rules_browsers+//browsers:extensions.bzl%browsers": {
"general": {
- "bzlTransitiveDigest": "w4tgtXA7WLl79lEQFfedhl7OVEGAKFqAFLnlI5c/Rtk=",
+ "bzlTransitiveDigest": "85AglD+Q1+ncEIApuW5Ng/2cY4OkSMPrOZ05Jp4CTmk=",
"usagesDigest": "FmXYJVoVJlnfUU8x8gObSvu4qWcco/9Faw61aC/wBF0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -548,9 +545,9 @@
"rules_browsers_chrome_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "7220f7149a01e85e631cb949b03fd472eba72fe0a92c6d8dab7833dd94e0c72c",
+ "sha256": "1a20882bf1a4f9d1167171b6905efe359bb229be1ce636ae9c931f3d285fbabe",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/linux64/chrome-headless-shell-linux64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/linux64/chrome-headless-shell-linux64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-linux64/chrome-headless-shell"
@@ -566,9 +563,9 @@
"rules_browsers_chrome_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "0d80d90379960943f3dc9f5eb966876010dd950736c97386e375581a56baf7c7",
+ "sha256": "1b0e0a078fc62c2b9f8cf837d41461084bd4064cf50cfc15ff84447a1f1b8dab",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/mac-x64/chrome-headless-shell-mac-x64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-x64/chrome-headless-shell-mac-x64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-x64/chrome-headless-shell"
@@ -584,9 +581,9 @@
"rules_browsers_chrome_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "7fad05ead762524231ed026cf40385cba80cb3daab2973622f3533308fc045fa",
+ "sha256": "7438714da3696778b697b277a2df1d9701b99819ae58f5ab809b1009933677ec",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/mac-arm64/chrome-headless-shell-mac-arm64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-arm64/chrome-headless-shell-mac-arm64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-arm64/chrome-headless-shell"
@@ -602,9 +599,9 @@
"rules_browsers_chrome_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "2c08f3f53115434e3dd623e2e1f1f54896fa4b2cbaf86518d7f4708dc80fe67d",
+ "sha256": "4349c800be1f063adc0f1a7e820d024b43ea73917a6893a3f6b3797a5200d49b",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/win64/chrome-headless-shell-win64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/win64/chrome-headless-shell-win64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-win64/chrome-headless-shell.exe"
@@ -620,9 +617,9 @@
"rules_browsers_chromedriver_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "8bab44616dd81669a7a99d77c6b7e952ee3ec3bfd01ddf864cf795a880cfbd92",
+ "sha256": "d275f01236831bd44c6b6712ba5bec0a028b1ce9473a6182aceae6657740a3d3",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/linux64/chromedriver-linux64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/linux64/chromedriver-linux64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-linux64/chromedriver"
@@ -636,9 +633,9 @@
"rules_browsers_chromedriver_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "d1f8618664ec5b0433c19b36eef91b55671a1adf595ffd012da8b859c32dc7b1",
+ "sha256": "cb331889191aa8ab11be6d5a3cc4de760f8cf63dbea5d1f22c80cf16ae68e46f",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/mac-x64/chromedriver-mac-x64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-x64/chromedriver-mac-x64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-mac-x64/chromedriver"
@@ -652,9 +649,9 @@
"rules_browsers_chromedriver_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "3c8b50af153cf6e8de8f2ec9f4b9a1ce717cd76a3ca6c82f9e736d7b705bdbe2",
+ "sha256": "1584dc9ae35f4a22c4ba4b142335d91d6d3096813395d3acaf135fb6e716ce8e",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/mac-arm64/chromedriver-mac-arm64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-arm64/chromedriver-mac-arm64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-mac-arm64/chromedriver"
@@ -668,9 +665,9 @@
"rules_browsers_chromedriver_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "2fdba8a5036173c57627b38df844e2f9c6634dd027eaa5a5f306033e14414e08",
+ "sha256": "061b4d1be9eb2a11ed036b3e562b565ebfd257fcd7cda3681a58e63bff5dd6d0",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/153.0.7981.3/win64/chromedriver-win64.zip"
+ "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/win64/chromedriver-win64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-win64/chromedriver.exe"
@@ -684,9 +681,9 @@
"rules_browsers_firefox_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "bfc57e7b6b4e6204b11e7e03c4b93cff708e9fb37f6b9948be243455311d82ee",
+ "sha256": "7665cd49ab13417270748325838e565136adbc76d41bbd76fb24d15a0cc7792b",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/153.0/linux-x86_64/en-US/firefox-153.0.tar.xz"
+ "https://archive.mozilla.org/pub/firefox/releases/154.0/linux-x86_64/en-US/firefox-154.0.tar.xz"
],
"named_files": {
"FIREFOX": "firefox/firefox"
@@ -700,9 +697,9 @@
"rules_browsers_firefox_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "2f9b5a20e546e7e79e4182f8fe10353a3e251635963ab3f6d399a3f290adeb96",
+ "sha256": "b0295d3b77ec632a60282cf2b9770e1dada085879d00e43d8d23fddf5015e06a",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/153.0/mac/en-US/Firefox%20153.0.dmg"
+ "https://archive.mozilla.org/pub/firefox/releases/154.0/mac/en-US/Firefox%20154.0.dmg"
],
"named_files": {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox"
@@ -716,9 +713,9 @@
"rules_browsers_firefox_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "2f9b5a20e546e7e79e4182f8fe10353a3e251635963ab3f6d399a3f290adeb96",
+ "sha256": "b0295d3b77ec632a60282cf2b9770e1dada085879d00e43d8d23fddf5015e06a",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/153.0/mac/en-US/Firefox%20153.0.dmg"
+ "https://archive.mozilla.org/pub/firefox/releases/154.0/mac/en-US/Firefox%20154.0.dmg"
],
"named_files": {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox"
@@ -732,9 +729,9 @@
"rules_browsers_firefox_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "b409ff5af3419214f11f928e67b1f44c1ad0f8a46f7db7e697caedefa02a6d2a",
+ "sha256": "6b3c996d27b50dca8e5b6dd2956233cc6bf4720cea3f0847a9423c2cd6759e73",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/153.0/win64/en-US/Firefox%20Setup%20153.0.exe"
+ "https://archive.mozilla.org/pub/firefox/releases/154.0/win64/en-US/Firefox%20Setup%20154.0.exe"
],
"named_files": {
"FIREFOX": "core/firefox.exe"
@@ -1175,7 +1172,7 @@
"@@yq.bzl+//yq:extensions.bzl%yq": {
"general": {
"bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=",
- "usagesDigest": "hKjC8oZmSZDlZ1NsPZgCEomqFtqBXEQo88wUuHfg4F4=",
+ "usagesDigest": "9kbJ6OfvQpAM3otgkGgS5xpOHOoVgV5ZJ9jSgMgUG2g=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
diff --git a/modules/testing/builder/package.json b/modules/testing/builder/package.json
index 1a7fa852247a..f2db890d966d 100644
--- a/modules/testing/builder/package.json
+++ b/modules/testing/builder/package.json
@@ -4,12 +4,12 @@
"@angular-devkit/build-angular": "workspace:*",
"@angular-devkit/core": "workspace:*",
"@angular/ssr": "workspace:*",
- "@vitest/coverage-v8": "4.1.10",
+ "@vitest/coverage-v8": "4.1.11",
"browser-sync": "3.0.4",
"istanbul-lib-instrument": "6.0.3",
"jsdom": "30.0.1",
- "ng-packagr": "22.2.0-next.2",
+ "ng-packagr": "22.2.0-next.3",
"rxjs": "7.8.2",
- "vitest": "4.1.10"
+ "vitest": "4.1.11"
}
}
diff --git a/package.json b/package.json
index 935b6cae3ecd..b4a082868bcb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@angular/devkit-repo",
- "version": "22.2.0-next.2",
+ "version": "22.2.0-next.5",
"private": true,
"description": "Software Development Kit for Angular",
"keywords": [
@@ -28,12 +28,12 @@
"type": "git",
"url": "git+https://github.com/angular/angular-cli.git"
},
- "packageManager": "pnpm@11.20.0",
+ "packageManager": "pnpm@11.24.0",
"engines": {
"node": "^22.22.3 || ^24.15.0 || >=26.0.0",
"npm": "Please use pnpm instead of NPM to install dependencies",
"yarn": "Please use pnpm instead of Yarn to install dependencies",
- "pnpm": "11.20.0"
+ "pnpm": "11.24.0"
},
"author": "Angular Authors",
"license": "MIT",
@@ -42,23 +42,23 @@
},
"homepage": "https://github.com/angular/angular-cli",
"dependencies": {
- "@angular/compiler-cli": "22.2.0-next.0",
+ "@angular/compiler-cli": "22.2.0-next.3",
"typescript": "6.0.3"
},
"devDependencies": {
- "@angular/animations": "22.2.0-next.0",
- "@angular/cdk": "22.1.0-rc.0",
- "@angular/common": "22.2.0-next.0",
- "@angular/compiler": "22.2.0-next.0",
- "@angular/core": "22.2.0-next.0",
- "@angular/forms": "22.2.0-next.0",
- "@angular/localize": "22.2.0-next.0",
- "@angular/material": "22.1.0-rc.0",
- "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#dcf9b6776377d5d301e054111add3ef80f433b70",
- "@angular/platform-browser": "22.2.0-next.0",
- "@angular/platform-server": "22.2.0-next.0",
- "@angular/router": "22.2.0-next.0",
- "@angular/service-worker": "22.2.0-next.0",
+ "@angular/animations": "22.2.0-next.3",
+ "@angular/cdk": "22.2.0-next.2",
+ "@angular/common": "22.2.0-next.3",
+ "@angular/compiler": "22.2.0-next.3",
+ "@angular/core": "22.2.0-next.3",
+ "@angular/forms": "22.2.0-next.3",
+ "@angular/localize": "22.2.0-next.3",
+ "@angular/material": "22.2.0-next.2",
+ "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#bcad06e14cb29b52b0856020af5e2ab868075602",
+ "@angular/platform-browser": "22.2.0-next.3",
+ "@angular/platform-server": "22.2.0-next.3",
+ "@angular/router": "22.2.0-next.3",
+ "@angular/service-worker": "22.2.0-next.3",
"@babel/core": "8.0.1",
"@bazel/bazelisk": "1.28.1",
"@bazel/buildifier": "8.2.1",
@@ -70,7 +70,7 @@
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "16.0.3",
- "@rollup/wasm-node": "4.62.4",
+ "@rollup/wasm-node": "4.62.5",
"@stylistic/eslint-plugin": "^5.0.0",
"@tony.ganchev/eslint-plugin-header": "~3.4.0",
"@types/babel__core": "7.20.5",
@@ -89,21 +89,20 @@
"@types/picomatch": "^4.0.0",
"@types/progress": "^2.0.3",
"@types/semver": "^7.3.12",
- "@types/watchpack": "^2.4.4",
"@types/yargs": "^17.0.20",
"@types/yargs-parser": "^21.0.0",
- "@typescript-eslint/eslint-plugin": "8.65.0",
- "@typescript-eslint/parser": "8.65.0",
+ "@typescript-eslint/eslint-plugin": "8.67.0",
+ "@typescript-eslint/parser": "8.67.0",
"ajv": "8.20.0",
"buffer": "6.0.3",
- "esbuild": "0.28.1",
- "esbuild-wasm": "0.28.1",
- "eslint": "10.8.0",
+ "esbuild": "0.28.2",
+ "esbuild-wasm": "0.28.2",
+ "eslint": "10.9.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"express": "5.2.1",
"fast-glob": "3.3.3",
- "globals": "17.9.0",
+ "globals": "17.11.0",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "4.2.0",
"husky": "9.1.7",
@@ -115,23 +114,23 @@
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
- "karma-jasmine-html-reporter": "~2.2.0",
+ "karma-jasmine-html-reporter": "~2.3.0",
"karma-source-map-support": "1.4.0",
"lodash": "^4.17.21",
- "magic-string": "1.1.0",
+ "magic-string": "1.2.2",
"prettier": "^3.0.0",
- "puppeteer": "25.4.0",
+ "puppeteer": "25.8.0",
"quicktype-core": "26.0.0",
- "rollup": "4.62.4",
+ "rollup": "4.62.5",
"rollup-license-plugin": "~3.2.0",
- "rollup-plugin-dts": "6.4.1",
+ "rollup-plugin-dts": "6.5.1",
"rollup-plugin-sourcemaps2": "0.5.8",
"semver": "7.8.5",
"source-map-support": "0.5.21",
"tslib": "2.8.1",
- "undici": "8.9.0",
+ "undici": "8.10.0",
"unenv": "^1.10.0",
- "verdaccio": "6.9.2",
+ "verdaccio": "6.10.0",
"verdaccio-auth-memory": "^13.0.0",
"zone.js": "^0.16.0"
},
diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel
index b2f0cdfd63f1..e63b084ec458 100644
--- a/packages/angular/build/BUILD.bazel
+++ b/packages/angular/build/BUILD.bazel
@@ -84,9 +84,11 @@ ts_project(
":node_modules/@babel/core",
":node_modules/@inquirer/confirm",
":node_modules/@oxc-project/types",
+ ":node_modules/@parcel/watcher",
":node_modules/@vitejs/plugin-basic-ssl",
":node_modules/beasties",
":node_modules/browserslist",
+ ":node_modules/chokidar",
":node_modules/https-proxy-agent",
":node_modules/istanbul-lib-instrument",
":node_modules/jsonc-parser",
@@ -104,11 +106,12 @@ ts_project(
":node_modules/rolldown",
":node_modules/rollup",
":node_modules/sass",
+ ":node_modules/sass-embedded",
":node_modules/source-map-support",
":node_modules/tinyglobby",
":node_modules/vite",
":node_modules/vitest",
- ":node_modules/watchpack",
+ ":node_modules/xxhash-wasm",
"//:node_modules/@angular/common",
"//:node_modules/@angular/compiler",
"//:node_modules/@angular/compiler-cli",
@@ -123,7 +126,6 @@ ts_project(
"//:node_modules/@types/node",
"//:node_modules/@types/picomatch",
"//:node_modules/@types/semver",
- "//:node_modules/@types/watchpack",
"//:node_modules/esbuild",
"//:node_modules/esbuild-wasm",
"//:node_modules/karma",
@@ -337,7 +339,7 @@ npm_package(
],
stamp_files = [
"src/utils/version.js",
- "src/tools/esbuild/utils.js",
+ "src/tools/esbuild/target.js",
"src/utils/normalize-cache.js",
"src/utils/supported-browsers.js",
],
diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json
index 633064827709..609cf3132ce0 100644
--- a/packages/angular/build/package.json
+++ b/packages/angular/build/package.json
@@ -21,27 +21,30 @@
"@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER",
"@babel/core": "8.0.1",
- "@inquirer/confirm": "6.1.1",
+ "@inquirer/confirm": "6.2.0",
+ "@parcel/watcher": "2.6.0",
"@vitejs/plugin-basic-ssl": "2.3.0",
"beasties": "0.4.3",
"browserslist": "^4.26.0",
- "esbuild": "0.28.1",
+ "chokidar": "5.0.0",
+ "esbuild": "0.28.2",
"https-proxy-agent": "9.1.0",
"jsonc-parser": "3.3.1",
"listr2": "11.0.0",
- "magic-string": "1.1.0",
+ "magic-string": "1.2.2",
"mrmime": "2.0.1",
- "oxc-parser": "0.142.0",
+ "oxc-parser": "0.146.0",
"parse5-html-rewriting-stream": "8.0.1",
"picomatch": "4.0.5",
- "piscina": "5.3.0",
- "rolldown": "1.2.1",
- "sass": "1.102.0",
+ "piscina": "5.3.1",
+ "rolldown": "1.2.5",
+ "sass": "1.103.1",
+ "sass-embedded": "1.103.1",
"semver": "7.8.5",
"source-map-support": "0.5.21",
"tinyglobby": "0.2.17",
- "vite": "8.2.0",
- "watchpack": "2.5.2"
+ "vite": "8.2.2",
+ "xxhash-wasm": "1.1.0"
},
"optionalDependencies": {
"lmdb": "3.5.6"
@@ -49,15 +52,15 @@
"devDependencies": {
"@angular-devkit/core": "workspace:*",
"@angular/ssr": "workspace:*",
- "@oxc-project/types": "0.142.0",
+ "@oxc-project/types": "0.146.0",
"istanbul-lib-instrument": "6.0.3",
"jsdom": "30.0.1",
- "less": "4.8.1",
- "ng-packagr": "22.2.0-next.2",
- "postcss": "8.5.25",
- "rollup": "4.62.4",
+ "less": "4.9.0",
+ "ng-packagr": "22.2.0-next.3",
+ "postcss": "8.5.26",
+ "rollup": "4.62.5",
"rxjs": "7.8.2",
- "vitest": "4.1.10"
+ "vitest": "4.1.11"
},
"peerDependencies": {
"@angular/compiler": "0.0.0-ANGULAR-FW-PEER-DEP",
diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts
index dbec8d687b9f..e71663765274 100644
--- a/packages/angular/build/src/builders/application/build-action.ts
+++ b/packages/angular/build/src/builders/application/build-action.ts
@@ -15,6 +15,7 @@ import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-lan
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
import { ChangedFiles } from '../../tools/esbuild/watcher';
import { shouldWatchRoot } from '../../utils/environment-options';
+import { initializeHash } from '../../utils/hash';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
import { toPosixPath } from '../../utils/path';
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
@@ -41,6 +42,7 @@ const packageWatchFiles = [
'.pnp.data.json',
];
+// eslint-disable-next-line max-lines-per-function
export async function* runEsBuildBuildAction(
action: (rebuildState?: RebuildState) => Promise,
options: {
@@ -78,6 +80,8 @@ export async function* runEsBuildBuildAction(
incrementalResults,
} = options;
+ await initializeHash();
+
const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;
// Initial build
@@ -104,20 +108,25 @@ export async function* runEsBuildBuildAction(
logger.info('Watch mode enabled. Watching for file changes...');
}
+ const normalizedOutputBase = toPosixPath(outputOptions.base);
+ const normalizedCacheBase = toPosixPath(cacheOptions.basePath);
const ignored: string[] = [
// Ignore the output and cache paths to avoid infinite rebuild cycles
- outputOptions.base,
- cacheOptions.basePath,
+ normalizedOutputBase,
+ `${normalizedOutputBase}/**`,
+ normalizedCacheBase,
+ `${normalizedCacheBase}/**`,
`${toPosixPath(workspaceRoot)}/**/.*/**`,
];
// Setup a watcher
const { createWatcher } = await import('../../tools/esbuild/watcher');
- watcher = createWatcher({
+ watcher = await createWatcher({
polling: typeof poll === 'number',
interval: poll,
followSymlinks: preserveSymlinks,
ignored,
+ cwd: workspaceRoot,
});
// Setup abort support
@@ -215,6 +224,9 @@ export async function* runEsBuildBuildAction(
// Remove any stale locations if the build was successful
if (staleWatchFiles?.size) {
watcher.remove([...staleWatchFiles]);
+ for (const staleFile of staleWatchFiles) {
+ currentWatchFiles.delete(staleFile);
+ }
}
for (const outputResult of emitOutputResults(
diff --git a/packages/angular/build/src/builders/application/chunk-optimizer.ts b/packages/angular/build/src/builders/application/chunk-optimizer.ts
index 7d7a6e56b829..ea4c2f8076f9 100644
--- a/packages/angular/build/src/builders/application/chunk-optimizer.ts
+++ b/packages/angular/build/src/builders/application/chunk-optimizer.ts
@@ -20,7 +20,7 @@
import type { Message, Metafile } from 'esbuild';
import assert from 'node:assert';
import type { Plugin } from 'rollup';
-import { BundleContextResult } from '../../tools/esbuild/bundler-context';
+import { BundleMergedContextResult } from '../../tools/esbuild/bundler-context';
import {
type BuildOutputFile,
BuildOutputFileType,
@@ -212,9 +212,9 @@ function createChunkOptimizationFailureMessage(message: string): Message {
*/
// eslint-disable-next-line max-lines-per-function
export async function optimizeChunks(
- original: BundleContextResult,
+ original: BundleMergedContextResult,
sourcemap: boolean | 'hidden',
-): Promise {
+): Promise {
// Failed builds cannot be optimized
if (original.errors) {
return original;
@@ -235,7 +235,7 @@ export async function optimizeChunks(
}
// No action required if no browser main entrypoint or metafile for stats
- if (!mainFile || !original.metafile) {
+ if (!mainFile || !original.metafiles.browser) {
return original;
}
@@ -340,9 +340,9 @@ export async function optimizeChunks(
}
// Update metafile
- const newMetafile = bundleOutputToEsbuildMetafile(optimizedOutput, original.metafile);
+ const newMetafile = bundleOutputToEsbuildMetafile(optimizedOutput, original.metafiles.browser);
// Add back the outputs that were not part of the optimization
- for (const [path, output] of Object.entries(original.metafile.outputs)) {
+ for (const [path, output] of Object.entries(original.metafiles.browser.outputs)) {
if (usedChunks.has(path)) {
continue;
}
@@ -350,11 +350,11 @@ export async function optimizeChunks(
newMetafile.outputs[path] = output;
for (const inputPath of Object.keys(output.inputs)) {
if (!newMetafile.inputs[inputPath]) {
- newMetafile.inputs[inputPath] = original.metafile.inputs[inputPath];
+ newMetafile.inputs[inputPath] = original.metafiles.browser.inputs[inputPath];
}
}
}
- original.metafile = newMetafile;
+ original.metafiles.browser = newMetafile;
// Remove used chunks and associated sourcemaps from the original result
original.outputFiles = original.outputFiles.filter(
@@ -418,6 +418,7 @@ export async function optimizeChunks(
const record = createInitialFileRecord(entryRecord.depth + 1);
+ original.initialFiles.set(importPath, record);
entriesToAnalyze.push([importPath, record]);
}
}
diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts
index faa61c6e523b..8fe488a8ff76 100644
--- a/packages/angular/build/src/builders/application/execute-build.ts
+++ b/packages/angular/build/src/builders/application/execute-build.ts
@@ -18,10 +18,11 @@ import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
import { LOCALE_DATA_BASE_MODULE } from '../../tools/esbuild/i18n-locale-plugin';
import { extractLicenses } from '../../tools/esbuild/license-extractor';
import { profileAsync } from '../../tools/esbuild/profiling';
+import { transformSupportedBrowsersToTargets } from '../../tools/esbuild/target';
import {
calculateEstimatedTransferSizes,
+ isZonelessApp,
logBuildStats,
- transformSupportedBrowsersToTargets,
} from '../../tools/esbuild/utils';
import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator';
import { optimizeChunksThreshold } from '../../utils/environment-options';
@@ -36,6 +37,9 @@ import { inlineI18n, loadActiveTranslations } from './i18n';
import { NormalizedApplicationBuildOptions } from './options';
import { createComponentStyleBundler, setupBundlerContexts } from './setup-bundling';
+/** The esbuild error text prefix used to detect top-level await errors. */
+const TOP_LEVEL_AWAIT_ERROR_TEXT =
+ 'Top-level await is not available in the configured target environment';
// eslint-disable-next-line max-lines-per-function
export async function executeBuild(
options: NormalizedApplicationBuildOptions,
@@ -71,7 +75,7 @@ export async function executeBuild(
let bundlerContexts;
let componentStyleBundler;
let codeBundleCache;
- let bundlingResult: BundleContextResult;
+ let bundlingIndividualResults: BundleContextResult[];
let templateUpdates: Map | undefined;
let angularCompilationContext: AngularCompilationContext | undefined;
let executionResult: ExecutionResult | undefined;
@@ -94,7 +98,7 @@ export async function executeBuild(
// Bundle all contexts that do not require TypeScript changed file checks.
// These will automatically use cached results based on the changed files.
- bundlingResult = await BundlerContext.bundleAll(
+ bundlingIndividualResults = await BundlerContext.bundleAll(
bundlerContexts.otherContexts,
allFileChanges,
);
@@ -108,7 +112,8 @@ export async function executeBuild(
const result = await typescriptContext.bundle(forceTypeScriptRebuild);
typescriptResults.push(result);
}
- bundlingResult = BundlerContext.mergeResults([bundlingResult, ...typescriptResults]);
+
+ bundlingIndividualResults.push(...typescriptResults);
} else {
const target = transformSupportedBrowsersToTargets(browsers);
codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined);
@@ -137,7 +142,7 @@ export async function executeBuild(
);
// Bundle everything on initial build
- bundlingResult = await BundlerContext.bundleAll([
+ bundlingIndividualResults = await BundlerContext.bundleAll([
...bundlerContexts.typescriptContexts,
...bundlerContexts.otherContexts,
]);
@@ -149,9 +154,11 @@ export async function executeBuild(
componentStyleBundler.invalidate(rebuildState.fileChanges.all);
const componentResults = await componentStyleBundler.bundleAllFiles(true, true);
- bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
+ bundlingIndividualResults.push(...componentResults);
}
+ let bundlingResult = BundlerContext.mergeResults(bundlingIndividualResults);
+
executionResult.addWarnings(bundlingResult.warnings);
// Add used external component style referenced files to be watched
@@ -173,6 +180,24 @@ export async function executeBuild(
// Return if the bundling has errors
if (bundlingResult.errors) {
+ // If Zone.js is used, augment top-level await errors with a more helpful message.
+ // esbuild's default error mentions "target environment" with browser versions, but
+ // the actual reason is that async/await is downleveled for Zone.js compatibility.
+ if (!isZonelessApp(options.polyfills)) {
+ for (const error of bundlingResult.errors) {
+ if (error.text?.startsWith(TOP_LEVEL_AWAIT_ERROR_TEXT)) {
+ error.notes ??= [];
+ error.notes.push({
+ text:
+ 'Top-level await is not supported in applications that use Zone.js. ' +
+ 'Consider removing Zone.js or moving this code into an async function. \n' +
+ 'For more information about zoneless Angular applications, visit: https://angular.dev/guide/zoneless',
+ location: null,
+ });
+ }
+ }
+ }
+
executionResult.addErrors(bundlingResult.errors);
return executionResult;
@@ -183,8 +208,8 @@ export async function executeBuild(
if (options.optimizationOptions.scripts) {
// Count lazy chunks (files not needed for initial load).
// Advanced chunk optimization is most beneficial when there are multiple lazy chunks.
- const { metafile, initialFiles } = bundlingResult;
- const lazyChunksCount = Object.keys(metafile.outputs).filter(
+ const { metafiles, initialFiles } = bundlingResult;
+ const lazyChunksCount = Object.keys(metafiles.browser.outputs || {}).filter(
(path) => path.endsWith('.js') && !initialFiles.has(path),
).length;
@@ -263,14 +288,19 @@ export async function executeBuild(
executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]);
}
- const { metafile, initialFiles, outputFiles } = bundlingResult;
+ const {
+ metafiles: { browser: browserMetafile, server: serverMetafile },
+ initialFiles,
+ outputFiles,
+ } = bundlingResult;
+ const metafiles = [browserMetafile, serverMetafile];
executionResult.outputFiles.push(...outputFiles);
// Analyze files for bundle budget failures if present
let budgetFailures: BudgetCalculatorResult[] | undefined;
if (options.budgets) {
- const compatStats = generateBudgetStats(metafile, outputFiles, initialFiles);
+ const compatStats = generateBudgetStats(browserMetafile, outputFiles, initialFiles);
budgetFailures = [...checkBudgets(options.budgets, compatStats, true)];
for (const { message, severity } of budgetFailures) {
if (severity === 'error') {
@@ -289,7 +319,7 @@ export async function executeBuild(
// Check metafile for CommonJS module usage if optimizing scripts
if (optimizationOptions.scripts) {
- const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies);
+ const messages = checkCommonJSModules(browserMetafile, options.allowedCommonJsDependencies);
executionResult.addWarnings(messages);
}
@@ -302,7 +332,7 @@ export async function executeBuild(
if (options.extractLicenses) {
executionResult.addOutputFile(
'3rdpartylicenses.txt',
- await extractLicenses(metafile, workspaceRoot),
+ await extractLicenses(metafiles, workspaceRoot),
BuildOutputFileType.Root,
);
}
@@ -325,13 +355,13 @@ export async function executeBuild(
// Perform i18n translation inlining if enabled
if (i18nOptions.shouldInline) {
- const result = await inlineI18n(metafile, options, executionResult, initialFiles);
+ const result = await inlineI18n(browserMetafile, options, executionResult, initialFiles);
executionResult.addErrors(result.errors);
executionResult.addWarnings(result.warnings);
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
} else {
const result = await executePostBundleSteps(
- metafile,
+ browserMetafile,
options,
executionResult.outputFiles,
executionResult.assetFiles,
@@ -349,19 +379,29 @@ export async function executeBuild(
executionResult.assetFiles.push(...result.additionalAssets);
}
- executionResult.addOutputFile(
- 'prerendered-routes.json',
- JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2),
- BuildOutputFileType.Root,
- );
+ if (serverEntryPoint) {
+ executionResult.addOutputFile(
+ 'prerendered-routes.json',
+ JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2),
+ BuildOutputFileType.Root,
+ );
+ }
- // Write metafile if stats option is enabled
+ // Write metafiles if stats option is enabled
if (options.stats) {
executionResult.addOutputFile(
- 'stats.json',
- JSON.stringify(metafile, null, 2),
+ 'browser-stats.json',
+ JSON.stringify(browserMetafile, null, 2),
BuildOutputFileType.Root,
);
+
+ if (serverEntryPoint) {
+ executionResult.addOutputFile(
+ 'server-stats.json',
+ JSON.stringify(serverMetafile, null, 2),
+ BuildOutputFileType.Root,
+ );
+ }
}
if (!jsonLogs && !options.quiet) {
@@ -369,7 +409,7 @@ export async function executeBuild(
rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
executionResult.addLog(
logBuildStats(
- metafile,
+ metafiles,
outputFiles,
initialFiles,
budgetFailures,
diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts
index 198071c5a280..cf76a14d9030 100644
--- a/packages/angular/build/src/builders/application/execute-post-bundle.ts
+++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts
@@ -36,7 +36,7 @@ import { OutputMode } from './schema';
/**
* Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation.
- * @param metafile An esbuild metafile object.
+ * @param browserMetafile An esbuild metafile object.
* @param options The normalized application builder options used to create the build.
* @param outputFiles The output files of an executed build.
* @param assetFiles The assets of an executed build.
@@ -45,7 +45,7 @@ import { OutputMode } from './schema';
*/
// eslint-disable-next-line max-lines-per-function
export async function executePostBundleSteps(
- metafile: Metafile,
+ browserMetafile: Metafile,
options: NormalizedApplicationBuildOptions,
outputFiles: BuildOutputFile[],
assetFiles: BuildOutputAsset[],
@@ -131,7 +131,7 @@ export async function executePostBundleSteps(
locale,
baseHref,
initialFilesPaths,
- metafile,
+ browserMetafile,
publicPath,
);
@@ -214,7 +214,7 @@ export async function executePostBundleSteps(
locale,
baseHref,
initialFilesPaths,
- metafile,
+ browserMetafile,
publicPath,
);
diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts
index c83f1a29a30a..f212659a14be 100644
--- a/packages/angular/build/src/builders/application/i18n.ts
+++ b/packages/angular/build/src/builders/application/i18n.ts
@@ -8,6 +8,8 @@
import { BuilderContext } from '@angular-devkit/architect';
import type { Metafile } from 'esbuild';
+import assert from 'node:assert';
+import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import {
ExecutionResult,
@@ -18,6 +20,7 @@ import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
import { maxWorkers } from '../../utils/environment-options';
import { loadTranslations } from '../../utils/i18n-options';
import { createTranslationLoader } from '../../utils/load-translations';
+import { createProjectResolver } from '../../utils/resolve-project';
import { executePostBundleSteps } from './execute-post-bundle';
import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options';
@@ -48,6 +51,7 @@ export async function inlineI18n(
outputFiles: executionResult.outputFiles,
shouldOptimize: optimizationOptions.scripts,
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
+ localizeVersion: i18nOptions.localizeVersion,
},
maxWorkers,
);
@@ -71,12 +75,29 @@ export async function inlineI18n(
);
try {
- for (const locale of i18nOptions.inlineLocales) {
- // A locale specific set of files is returned from the inliner.
- const localeInlineResult = await inliner.inlineForLocale(
+ const localesToInline = Array.from(i18nOptions.inlineLocales, (locale) => {
+ const localeDescription = i18nOptions.locales[locale];
+ let translationIntegrity: string | undefined = '';
+ for (const file of localeDescription.files) {
+ if (!file.integrity) {
+ translationIntegrity = undefined;
+ break;
+ }
+ translationIntegrity += (translationIntegrity ? '|' : '') + file.integrity;
+ }
+
+ return {
locale,
- i18nOptions.locales[locale].translation,
- );
+ translation: localeDescription.translation,
+ translationIntegrity,
+ };
+ });
+
+ const inlinedLocales = await inliner.inlineAll(localesToInline);
+
+ for (const locale of i18nOptions.inlineLocales) {
+ const localeInlineResult = inlinedLocales.get(locale);
+ assert(localeInlineResult !== undefined, 'Inlined result must exist for locale: ' + locale);
const localeOutputFiles = localeInlineResult.outputFiles;
inlineResult.errors.push(...localeInlineResult.errors);
inlineResult.warnings.push(...localeInlineResult.warnings);
@@ -176,6 +197,15 @@ export async function loadActiveTranslations(
context: BuilderContext,
i18n: NormalizedApplicationBuildOptions['i18nOptions'],
) {
+ if (!i18n.localizeVersion) {
+ try {
+ const projectResolve = createProjectResolver(context.workspaceRoot);
+ const manifestPath = projectResolve('@angular/localize/package.json');
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf-8')) as { version?: string };
+ i18n.localizeVersion = manifest.version;
+ } catch {}
+ }
+
// Load locale data and translations (if present)
let loader;
for (const [locale, desc] of Object.entries(i18n.locales)) {
diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts
index b3c180843f70..abe3298aa093 100644
--- a/packages/angular/build/src/builders/application/options.ts
+++ b/packages/angular/build/src/builders/application/options.ts
@@ -126,6 +126,20 @@ interface InternalOptions {
* Suppress build summary and stats table.
*/
quiet?: boolean;
+
+ /**
+ * Disables esbuild code splitting for the browser code bundle.
+ *
+ * Splitting emits shared chunks whose exports are read across chunk boundaries as live ESM
+ * bindings. A module hoisted into a shared chunk is wrapped in a lazy initializer, so its exported
+ * value is only assigned once that initializer runs. Runners that load the generated output
+ * through a module runner rather than the browser's own ESM implementation do not reliably
+ * preserve those bindings, and an importing chunk can observe the export as `undefined`.
+ *
+ * Test bundles are never downloaded by a browser, so there is nothing for splitting to optimize
+ * there. Used exclusively for tests and shouldn't be used for other kinds of builds.
+ */
+ disableCodeSplitting?: boolean;
}
/** Full set of options for `application` builder. */
@@ -439,6 +453,7 @@ export async function normalizeOptions(
partialSSRBuild = false,
externalRuntimeStyles,
instrumentForCoverage,
+ disableCodeSplitting,
} = options;
// Return all the normalized options
@@ -475,6 +490,7 @@ export async function normalizeOptions(
watch,
workspaceRoot,
entryPoints,
+ disableCodeSplitting,
optimizationOptions,
outputOptions,
outExtension,
diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts
index bb53c0bae902..4380db080e87 100644
--- a/packages/angular/build/src/builders/application/setup-bundling.ts
+++ b/packages/angular/build/src/builders/application/setup-bundling.ts
@@ -19,7 +19,8 @@ import {
import { BundlerContext } from '../../tools/esbuild/bundler-context';
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles';
-import { getSupportedNodeTargets } from '../../tools/esbuild/utils';
+import { MemoryLoadResultCache } from '../../tools/esbuild/load-result-cache';
+import { getSupportedNodeTargets } from '../../tools/esbuild/target';
import type { NormalizedApplicationBuildOptions } from './options';
/**
@@ -94,11 +95,19 @@ export function setupBundlerContexts(
// Global Stylesheets
if (options.globalStyles.length > 0) {
+ const globalStylesCache = new MemoryLoadResultCache();
for (const initial of [true, false]) {
const bundleOptions = createGlobalStylesBundleOptions(options, target, initial);
if (bundleOptions) {
otherContexts.push(
- new BundlerContext(workspaceRoot, watch, bundleOptions, true, () => initial),
+ new BundlerContext(
+ workspaceRoot,
+ watch,
+ bundleOptions,
+ true,
+ () => initial,
+ globalStylesCache,
+ ),
);
}
}
diff --git a/packages/angular/build/src/builders/application/tests/behavior/top-level-await-error_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/top-level-await-error_spec.ts
new file mode 100644
index 000000000000..b0220529ee28
--- /dev/null
+++ b/packages/angular/build/src/builders/application/tests/behavior/top-level-await-error_spec.ts
@@ -0,0 +1,67 @@
+/**
+ * @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 { buildApplication } from '../../index';
+import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
+
+describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
+ describe('Behavior: "Top-level await error message"', () => {
+ it('should show a Zone.js-specific error when top-level await is used with Zone.js', async () => {
+ await harness.writeFile(
+ 'src/main.ts',
+ `
+ // The export makes this file a module, which is required for top-level await.
+ export const value = await Promise.resolve('test');
+ console.log(value);
+ `,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ polyfills: ['zone.js'],
+ });
+
+ const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(
+ 'Top-level await is not supported in applications that use Zone.js',
+ ),
+ }),
+ );
+ });
+
+ it('should not show a Zone.js-specific error when top-level await is used without Zone.js', async () => {
+ await harness.writeFile(
+ 'src/main.ts',
+ `
+ // The export makes this file a module, which is required for top-level await.
+ export const value = await Promise.resolve('test');
+ console.log(value);
+ `,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ polyfills: [],
+ });
+
+ const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
+ expect(result?.success).toBeTrue();
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ level: 'error',
+ message: jasmine.stringContaining(
+ 'Top-level await is not supported in applications that use Zone.js',
+ ),
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts b/packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts
new file mode 100644
index 000000000000..f38ae4996d16
--- /dev/null
+++ b/packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts
@@ -0,0 +1,103 @@
+/**
+ * @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 { buildApplication } from '../../index';
+import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
+
+describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
+ describe('Option: "statsJson"', () => {
+ it('generates only browser stats file containing valid metafile data when true', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ statsJson: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/browser-stats.json').toExist();
+ harness.expectFile('dist/server-stats.json').toNotExist();
+
+ const browserStats = JSON.parse(harness.readFile('dist/browser-stats.json'));
+ expect(browserStats.inputs).toBeDefined();
+ expect(browserStats.outputs).toBeDefined();
+ expect(Object.keys(browserStats.outputs).length).toBeGreaterThan(0);
+ });
+
+ it('does not generate stats files when false', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ statsJson: false,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+ harness.expectFile('dist/browser-stats.json').toNotExist();
+ harness.expectFile('dist/server-stats.json').toNotExist();
+ });
+
+ it('does not generate stats files when not set', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+ harness.expectFile('dist/browser-stats.json').toNotExist();
+ harness.expectFile('dist/server-stats.json').toNotExist();
+ });
+
+ describe('server build', () => {
+ beforeEach(async () => {
+ await harness.modifyFile('src/tsconfig.app.json', (content) => {
+ const tsConfig = JSON.parse(content);
+ tsConfig.files ??= [];
+ tsConfig.files.push('main.server.ts');
+
+ return JSON.stringify(tsConfig);
+ });
+ });
+
+ it('generates separated browser and server stats files for an SSR build', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ server: 'src/main.server.ts',
+ ssr: true,
+ statsJson: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/browser-stats.json').toExist();
+ harness.expectFile('dist/server-stats.json').toExist();
+
+ const browserStats = JSON.parse(harness.readFile('dist/browser-stats.json'));
+ const serverStats = JSON.parse(harness.readFile('dist/server-stats.json'));
+
+ const browserPaths = new Set(Object.keys(browserStats.outputs));
+ const serverPaths = new Set(Object.keys(serverStats.outputs));
+
+ expect(serverPaths.size).toBeGreaterThan(0);
+ expect(browserPaths.size).toBeGreaterThan(0);
+
+ for (const path of serverPaths) {
+ expect(browserPaths.has(path))
+ .withContext(`Server output '${path}' should not appear in browser-stats.json`)
+ .toBeFalse();
+ }
+
+ for (const path of browserPaths) {
+ expect(serverPaths.has(path))
+ .withContext(`Browser output '${path}' should not appear in server-stats.json`)
+ .toBeFalse();
+ }
+ });
+ });
+ });
+});
diff --git a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts
index e846053e64c4..ca9212b0d3e3 100644
--- a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts
+++ b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts
@@ -238,5 +238,21 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
.withContext('importmap must precede the first module script tag')
.toBeLessThan(moduleScriptIdx);
});
+
+ it(`preserves integrity and crossorigin in autoCsp loader when autoCsp is also enabled`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ subresourceIntegrity: true,
+ security: { autoCsp: true },
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+
+ const indexHtml = harness.readFile('dist/browser/index.html');
+ expect(indexHtml).toMatch(
+ /const scripts = \[\[(?:'[^']+', 'module', false, false, "sha384-[^"]+", "anonymous"(?:, )?)+\]\];/,
+ );
+ });
});
});
diff --git a/packages/angular/build/src/builders/dev-server/internal.ts b/packages/angular/build/src/builders/dev-server/internal.ts
index 4e5e97e5cf14..96587acb5cff 100644
--- a/packages/angular/build/src/builders/dev-server/internal.ts
+++ b/packages/angular/build/src/builders/dev-server/internal.ts
@@ -13,7 +13,7 @@ export { getFeatureSupport, isZonelessApp } from '../../tools/esbuild/utils';
export { type IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
export { purgeStaleBuildCache } from '../../utils/purge-cache';
export { getSupportedBrowsers } from '../../utils/supported-browsers';
-export { transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils';
+export { transformSupportedBrowsersToTargets } from '../../tools/esbuild/target';
export { buildApplicationInternal } from '../../builders/application';
export type { ApplicationBuilderInternalOptions } from '../../builders/application/options';
export type { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result';
diff --git a/packages/angular/build/src/builders/dev-server/vite/index.ts b/packages/angular/build/src/builders/dev-server/vite/index.ts
index eeffb6531969..94192ec41b8c 100644
--- a/packages/angular/build/src/builders/dev-server/vite/index.ts
+++ b/packages/angular/build/src/builders/dev-server/vite/index.ts
@@ -389,15 +389,11 @@ export async function* serveWithVite(
? browserOptions.polyfills
: [browserOptions.polyfills];
- // TODO(alanagius): This is a workaround for https://github.com/rolldown/rolldown/issues/10633
- const target = isZonelessApp(polyfills) ? ['es2022'] : ['es2016'];
-
- // Once the above issue is fixed, uncomment the below code.
- // const target = transformSupportedBrowsersToTargets(browsers);
- // if (!isZonelessApp(polyfills)) {
- // // Rolldown doesn't have an option to support Zone.js/async-await, so we need to support es2016.
- // target.push('es2016');
- // }
+ const target = transformSupportedBrowsersToTargets(browsers);
+ if (!isZonelessApp(polyfills)) {
+ // Rolldown doesn't have an option to support Zone.js/async-await, so we need to support es2016.
+ target.push('es2016');
+ }
let ssrMode: ServerSsrMode = ServerSsrMode.NoSsr;
if (
diff --git a/packages/angular/build/src/builders/dev-server/vite/server.ts b/packages/angular/build/src/builders/dev-server/vite/server.ts
index 868590edab95..b1826383fccb 100644
--- a/packages/angular/build/src/builders/dev-server/vite/server.ts
+++ b/packages/angular/build/src/builders/dev-server/vite/server.ts
@@ -22,6 +22,7 @@ import {
} from '../../../tools/vite/plugins';
import { RolldownLoaderOption, getDepOptimizationConfig } from '../../../tools/vite/utils';
import { loadProxyConfiguration } from '../../../utils';
+import { initializeHash } from '../../../utils/hash';
import { type ApplicationBuilderInternalOptions, JavaScriptTransformer } from '../internal';
import type { NormalizedDevServerOptions } from '../options';
import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord } from './utils';
@@ -147,6 +148,7 @@ export async function setupServer(
indexHtmlTransformer?: (content: string) => Promise,
thirdPartySourcemaps = false,
): Promise {
+ await initializeHash();
const { normalizePath } = (await import('vite' as string)) as typeof Vite;
// Path will not exist on disk and only used to provide separate path for Vite requests
diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts
index 1206bb2c1f21..7d57716214b1 100644
--- a/packages/angular/build/src/builders/unit-test/options.ts
+++ b/packages/angular/build/src/builders/unit-test/options.ts
@@ -10,7 +10,7 @@ import { type BuilderContext, targetFromTargetString } from '@angular-devkit/arc
import { constants, promises as fs } from 'node:fs';
import path from 'node:path';
import { normalizeCacheOptions } from '../../utils/normalize-cache';
-import { canonicalizePath } from '../../utils/path';
+import { canonicalizePath, toPosixPath } from '../../utils/path';
import { getProjectRootPaths } from '../../utils/project-metadata';
import { isTTY } from '../../utils/tty';
import { Runner, type Schema as UnitTestBuilderOptions } from './schema';
@@ -143,7 +143,7 @@ export async function normalizeOptions(
quiet: options.quiet ?? (process.env['CI'] ? false : true),
providersFile: options.providersFile && path.join(workspaceRoot, options.providersFile),
setupFiles: options.setupFiles
- ? options.setupFiles.map((setupFile) => path.join(workspaceRoot, setupFile))
+ ? options.setupFiles.map((setupFile) => toPosixPath(path.join(workspaceRoot, setupFile)))
: [],
dumpVirtualFiles: options.dumpVirtualFiles,
listTests: options.listTests,
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 7f0f67fc0c2e..3936f44b09fd 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
@@ -257,6 +257,12 @@ export async function getVitestBuildOptions(
outputHashing: adjustOutputHashing(baseBuildOptions.outputHashing),
optimization: false,
entryPoints,
+ // Every spec file is its own entry point, so splitting hoists any module shared between two
+ // specs into a chunk whose exports are then read across a chunk boundary. Those reads rely on
+ // live ESM bindings, and a module placed in a shared chunk is only assigned its exported value
+ // when that chunk's lazy initializer runs, so an importing chunk can read `undefined`. Nothing
+ // downloads these bundles, so there is no benefit to weigh against that.
+ disableCodeSplitting: true,
// Enable support for vitest browser prebundling. Excludes can be controlled with a runnerConfig
// and the `optimizeDeps.exclude` option.
externalPackages: true,
diff --git a/packages/angular/build/src/builders/unit-test/test-discovery.ts b/packages/angular/build/src/builders/unit-test/test-discovery.ts
index 7bad7079dc90..f2fc8c221646 100644
--- a/packages/angular/build/src/builders/unit-test/test-discovery.ts
+++ b/packages/angular/build/src/builders/unit-test/test-discovery.ts
@@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import { createHash } from 'node:crypto';
import { type PathLike, constants, promises as fs } from 'node:fs';
import os from 'node:os';
import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path';
import { glob, isDynamicPattern } from 'tinyglobby';
+import { calculateHash, initializeHash } from '../../utils/hash';
import { toPosixPath } from '../../utils/path';
/**
@@ -41,6 +41,7 @@ export async function findTests(
workspaceRoot: string,
projectSourceRoot: string,
): Promise {
+ await initializeHash();
const resolvedTestFiles = new Set();
const dynamicPatterns: string[] = [];
@@ -194,7 +195,7 @@ function truncateName(name: string, originalPath: string): string {
return name;
}
- const hash = createHash('sha256').update(originalPath).digest('hex').substring(0, 8);
+ const hash = calculateHash(originalPath).substring(0, 8);
const availableLength = MAX_FILENAME_LENGTH - hash.length - 2; // 2 for '-' separators
const prefixLength = Math.floor(availableLength / 2);
const suffixLength = availableLength - prefixLength;
diff --git a/packages/angular/build/src/builders/unit-test/test-discovery_spec.ts b/packages/angular/build/src/builders/unit-test/test-discovery_spec.ts
index dcee1718a976..764924d9552b 100644
--- a/packages/angular/build/src/builders/unit-test/test-discovery_spec.ts
+++ b/packages/angular/build/src/builders/unit-test/test-discovery_spec.ts
@@ -6,9 +6,14 @@
* found in the LICENSE file at https://angular.dev/license
*/
+import { initializeHash } from '../../utils/hash';
import { generateNameFromPath, getTestEntrypoints } from './test-discovery';
describe('getTestEntrypoints', () => {
+ beforeAll(async () => {
+ await initializeHash();
+ });
+
const workspaceRoot = '/project';
const projectSourceRoot = '/project/src';
const options = { workspaceRoot, projectSourceRoot };
@@ -81,6 +86,10 @@ describe('getTestEntrypoints', () => {
});
describe('generateNameFromPath', () => {
+ beforeAll(async () => {
+ await initializeHash();
+ });
+
const roots = ['/project/src/', '/project/'];
it('should generate a dash-cased name from a simple path', () => {
@@ -127,7 +136,7 @@ describe('generateNameFromPath', () => {
expect(result.length).toBeLessThanOrEqual(128);
expect(result).toBe(
- 'a-very-long-path-that-definitely-exceeds-the-maximum-allowe-9cf40291-me-in-order-to-trigger-the-truncation-logic-in-the-function',
+ 'a-very-long-path-that-definitely-exceeds-the-maximum-allowe-4af8113d-me-in-order-to-trigger-the-truncation-logic-in-the-function',
); // eslint-disable-line max-len
});
diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-barrel-side-effects_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-barrel-side-effects_spec.ts
new file mode 100644
index 000000000000..7a59f09eeaa0
--- /dev/null
+++ b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-barrel-side-effects_spec.ts
@@ -0,0 +1,134 @@
+/**
+ * @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 { execute } from '../../index';
+import { BASE_OPTIONS, describeBuilder, UNIT_TEST_BUILDER_INFO } from '../setup';
+
+describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
+ describe('Behavior: "Vitest barrel sideEffects"', () => {
+ // Regression test for https://github.com/angular/angular-cli/issues/33910.
+ it('should retain barrel-exported components when package.json has sideEffects: false', async () => {
+ harness.withBuilderTarget(
+ 'build',
+ async () => ({ success: true }),
+ {
+ project: 'src/lib/ng-package.json',
+ },
+ {
+ builderName: '@angular/build:ng-packagr',
+ },
+ );
+
+ await harness.modifyFile('tsconfig.json', (content) => {
+ const tsconfig = JSON.parse(content);
+ tsconfig.compilerOptions ??= {};
+ tsconfig.compilerOptions.paths = {
+ '@lib': ['./src/lib/public-api.ts'],
+ '@lib/sub': ['./src/lib/sub/public-api.ts'],
+ };
+
+ return JSON.stringify(tsconfig);
+ });
+
+ await harness.writeFiles({
+ 'src/lib/package.json': JSON.stringify({
+ name: '@lib',
+ version: '0.0.1',
+ sideEffects: false,
+ }),
+ 'src/lib/ng-package.json': JSON.stringify({
+ lib: {
+ entryFile: 'public-api.ts',
+ },
+ }),
+ 'src/lib/public-api.ts': `
+ export * from './components/public-api-components';
+ export * from './other/other.component';
+ `,
+ 'src/lib/components/public-api-components.ts': `export * from './foo/foo.component';`,
+ 'src/lib/components/foo/foo.component.ts': `
+ import { Component } from '@angular/core';
+
+ @Component({
+ selector: 'lib-foo',
+ standalone: true,
+ template: 'foo
',
+ })
+ export class FooComponent {}
+ `,
+ 'src/lib/other/other.component.ts': `
+ import { Component } from '@angular/core';
+
+ @Component({
+ selector: 'lib-other',
+ standalone: true,
+ template: 'other
',
+ })
+ export class OtherComponent {}
+ `,
+ 'src/lib/other/other.component.spec.ts': `
+ import { TestBed } from '@angular/core/testing';
+ import { describe, it, expect } from 'vitest';
+ import { OtherComponent } from '@lib';
+
+ describe('OtherComponent', () => {
+ it('creates', () => {
+ TestBed.configureTestingModule({
+ imports: [OtherComponent],
+ });
+ const fixture = TestBed.createComponent(OtherComponent);
+ expect(fixture).toBeTruthy();
+ });
+ });
+ `,
+ 'src/lib/sub/ng-package.json': JSON.stringify({
+ lib: {
+ entryFile: 'public-api.ts',
+ },
+ }),
+ 'src/lib/sub/public-api.ts': `export * from './bar/bar.component';`,
+ 'src/lib/sub/bar/bar.component.ts': `
+ import { Component } from '@angular/core';
+ import { FooComponent } from '@lib';
+
+ @Component({
+ selector: 'lib-bar',
+ standalone: true,
+ imports: [FooComponent],
+ template: '',
+ })
+ export class BarComponent {}
+ `,
+ 'src/lib/sub/bar/bar.component.spec.ts': `
+ import { TestBed } from '@angular/core/testing';
+ import { describe, it, expect } from 'vitest';
+ import { BarComponent } from './bar.component';
+
+ describe('BarComponent', () => {
+ it('creates', () => {
+ TestBed.configureTestingModule({
+ imports: [BarComponent],
+ });
+ const fixture = TestBed.createComponent(BarComponent);
+ expect(fixture).toBeTruthy();
+ });
+ });
+ `,
+ });
+
+ harness.useTarget('test', {
+ ...BASE_OPTIONS,
+ include: ['lib/**/*.spec.ts'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+ });
+ });
+});
diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-shared-chunk-init_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-shared-chunk-init_spec.ts
new file mode 100644
index 000000000000..13870ba36eba
--- /dev/null
+++ b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-shared-chunk-init_spec.ts
@@ -0,0 +1,172 @@
+/**
+ * @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 { execute } from '../../index';
+import {
+ BASE_OPTIONS,
+ describeBuilder,
+ UNIT_TEST_BUILDER_INFO,
+ setupApplicationTarget,
+} from '../setup';
+
+describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
+ describe('Behavior: "Vitest shared chunk initialization"', () => {
+ // Regression test for https://github.com/angular/angular-cli/issues/33728.
+ //
+ // Without `disableCodeSplitting`, esbuild hoists a module imported by more than one spec
+ // entry point into a shared chunk behind a lazy `__esm` initializer, and a class-field
+ // initializer in another chunk reads the exported value as `undefined` under the jsdom
+ // runner. All four trigger conditions are required and encoded below:
+ // 1. two spec entry points import the shared module (so it lands in a shared chunk);
+ // 2. a component in one entry reads the export during class-field initialization;
+ // 3. that component's spec file contains an `async` test callback (no `await` needed);
+ // 4. zone.js is in the polyfills (the `setupApplicationTarget` default), which downlevels
+ // async and makes esbuild emit the spec entry CommonJS-wrapped.
+ //
+ // NOTE: the failure this guards against is sensitive to inert content — adding a top-level
+ // side effect (even a `console.log`) to the shared or importing module below defused it
+ // during reduction. Mirror https://github.com/jonmarozick/ng-shared-chunk-repro when
+ // modifying these fixtures.
+ it('should provide shared-module exports to class-field initializers in async specs', async () => {
+ setupApplicationTarget(harness);
+
+ harness.useTarget('test', {
+ ...BASE_OPTIONS,
+ });
+
+ // Keep the default project's spec deterministic; a third spec entry that does not touch
+ // the shared module does not affect the reproduction (verified in a fresh workspace).
+ await harness.writeFile(
+ 'src/app/app.component.spec.ts',
+ `
+ import { describe, it, expect } from 'vitest';
+
+ describe('AppComponent placeholder', () => {
+ it('runs', () => {
+ expect(1 + 1).toBe(2);
+ });
+ });
+ `,
+ );
+
+ // The shared `const`. Reached from both spec entry points, so it is hoisted into a chunk
+ // shared between them.
+ await harness.writeFile(
+ 'src/environments/env-config.ts',
+ `
+ export interface DealerConfig {
+ dealerId: string;
+ clientKey: string;
+ }
+
+ export const DEALERS: DealerConfig[] = [
+ { dealerId: 'dealer-one', clientKey: 'KEY-ONE' },
+ { dealerId: 'dealer-two', clientKey: 'KEY-TWO' },
+ { dealerId: 'dealer-three', clientKey: 'KEY-THREE' },
+ { dealerId: 'dealer-four', clientKey: 'KEY-FOUR' },
+ ];
+ `,
+ );
+
+ // Reached by both spec entries, so it and env-config.ts land in the shared chunk. Reads the
+ // const inside a method — after module initialization — and is the passing control.
+ await harness.writeFile(
+ 'src/app/features/lead-generator/services/lead.service.ts',
+ `
+ import { Injectable } from '@angular/core';
+
+ import { DEALERS } from '../../../../environments/env-config';
+
+ @Injectable({ providedIn: 'root' })
+ export class LeadService {
+ resolveClientKey(dealerId: string, clientKey?: string): string {
+ return clientKey ?? DEALERS.find((d) => d.dealerId === dealerId)?.clientKey ?? '';
+ }
+ }
+ `,
+ );
+
+ await harness.writeFile(
+ 'src/app/features/lead-generator/services/index.ts',
+ `export * from './lead.service';\n`,
+ );
+
+ // Spec entry point 1 — the second importer that causes the chunk to be shared at all.
+ await harness.writeFile(
+ 'src/app/features/lead-generator/services/lead.service.spec.ts',
+ `
+ import { describe, it, expect } from 'vitest';
+ import { TestBed } from '@angular/core/testing';
+
+ import { LeadService } from './lead.service';
+
+ describe('LeadService', () => {
+ it('reads DEALERS inside a method', () => {
+ TestBed.configureTestingModule({ providers: [LeadService] });
+
+ expect(TestBed.inject(LeadService).resolveClientKey('dealer-one')).toBe('KEY-ONE');
+ });
+ });
+ `,
+ );
+
+ // In the other chunk; reads the shared export eagerly during class-field initialization.
+ await harness.writeFile(
+ 'src/app/features/lead-generator/lead-generator.container.ts',
+ `
+ import { Component, inject } from '@angular/core';
+
+ import { LeadService } from './services';
+ import { DEALERS, DealerConfig } from '../../../environments/env-config';
+
+ @Component({
+ selector: 'app-lead-generator',
+ standalone: true,
+ template: '',
+ })
+ export class LeadGeneratorContainer {
+ private readonly leadService = inject(LeadService);
+
+ readonly dealers: DealerConfig[] = DEALERS;
+ readonly dealerOptions = this.dealers.map((d) => d.dealerId);
+
+ hasService(): boolean {
+ return this.leadService != null;
+ }
+ }
+ `,
+ );
+
+ // Spec entry point 2 — the failing case without the fix. The `async` is load-bearing:
+ // zone.js makes the builder downlevel it, the spec then imports the `__async` helper, and
+ // esbuild emits this entry CommonJS-wrapped with the component module behind a lazy
+ // `__esm` initializer. No `await` is needed; a synchronous callback hides the defect.
+ await harness.writeFile(
+ 'src/app/features/lead-generator/lead-generator.container.spec.ts',
+ `
+ import { describe, it, expect } from 'vitest';
+ import { TestBed } from '@angular/core/testing';
+
+ import { LeadGeneratorContainer } from './lead-generator.container';
+
+ describe('LeadGeneratorContainer', () => {
+ it('reads DEALERS in a class-field initialiser', async () => {
+ const fixture = TestBed.createComponent(LeadGeneratorContainer);
+
+ expect(fixture.componentInstance.dealers.length).toBe(4);
+ });
+ });
+ `,
+ );
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+ });
+ });
+});
diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts
index f1eb9dccb459..315636decae2 100644
--- a/packages/angular/build/src/private.ts
+++ b/packages/angular/build/src/private.ts
@@ -34,8 +34,8 @@ export {
// Tools
export type { ExternalResultMetadata } from './tools/esbuild/bundler-execution-result';
export { emitFilesToDisk } from './tools/esbuild/utils';
-export { transformSupportedBrowsersToTargets } from './tools/esbuild/utils';
-export { SassWorkerImplementation } from './tools/sass/sass-service';
+export { transformSupportedBrowsersToTargets } from './tools/esbuild/target';
+export { SassWorkerImplementation } from './tools/sass/sass-worker-implementation';
export { SourceFileCache } from './tools/esbuild/angular/source-file-cache';
export { Cache } from './tools/esbuild/cache';
diff --git a/packages/angular/build/src/tools/angular/angular-host.ts b/packages/angular/build/src/tools/angular/angular-host.ts
index 874d66fe2b41..9322b5683dc5 100644
--- a/packages/angular/build/src/tools/angular/angular-host.ts
+++ b/packages/angular/build/src/tools/angular/angular-host.ts
@@ -8,16 +8,15 @@
import type * as ng from '@angular/compiler-cli';
import assert from 'node:assert';
-import { createHash } from 'node:crypto';
import nodePath from 'node:path';
import type ts from 'typescript';
+import { calculateHash } from '../../utils/hash';
export type AngularCompilerOptions = ng.CompilerOptions;
export type AngularCompilerHost = ng.CompilerHost;
export interface AngularHostOptions {
fileReplacements?: Record;
- sourceFileCache?: Map;
modifiedFiles?: Set;
externalStylesheets?: Map;
transformStylesheet(
@@ -46,7 +45,7 @@ export function ensureSourceFileVersions(program: ts.Program): void {
for (const file of files) {
if (file.version === undefined) {
- file.version = createHash('sha256').update(file.text).digest('hex');
+ file.version = calculateHash(file.text);
}
}
@@ -165,6 +164,7 @@ export function createAngularCompilerHost(
compilerOptions: AngularCompilerOptions,
hostOptions: AngularHostOptions,
packageJsonCache: ts.PackageJsonInfoCache | undefined,
+ sourceFileCache?: Map,
): AngularCompilerHost {
// Create TypeScript compiler host
const host: AngularCompilerHost = typescript.createIncrementalCompilerHost(compilerOptions);
@@ -227,7 +227,7 @@ export function createAngularCompilerHost(
// For external stylesheets, create a unique identifier and store the mapping
let externalId = hostOptions.externalStylesheets.get(resolvedPath);
if (externalId === undefined) {
- externalId = createHash('sha256').update(resolvedPath).digest('hex');
+ externalId = calculateHash(resolvedPath);
hostOptions.externalStylesheets.set(resolvedPath, externalId);
}
@@ -254,8 +254,8 @@ export function createAngularCompilerHost(
}
// Augment TypeScript Host with source file caching if provided
- if (hostOptions.sourceFileCache) {
- augmentHostWithCaching(host, hostOptions.sourceFileCache);
+ if (sourceFileCache) {
+ augmentHostWithCaching(host, sourceFileCache);
}
return host;
diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts
index 00a17ccc453e..4c17bb6e60de 100644
--- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts
@@ -19,6 +19,28 @@ export interface EmitFileResult {
dependencies?: readonly string[];
}
+export interface FileTransformResult {
+ contents: string;
+ watchFiles?: readonly string[];
+}
+
+export interface AngularCompilationOptions {
+ allowJs?: boolean;
+ isolatedModules?: boolean;
+ sourceMap?: boolean;
+ inlineSourceMap?: boolean;
+ _useTypeScriptTranspilation?: boolean;
+ [key: string]: unknown;
+}
+
+export interface AngularCompilationResult {
+ compilerOptions: AngularCompilationOptions;
+ referencedFiles: readonly string[];
+ externalStylesheets?: ReadonlyMap;
+ templateUpdates?: ReadonlyMap;
+ componentResourcesDependencies?: ReadonlyMap;
+}
+
export enum DiagnosticModes {
None = 0,
Option = 1 << 0,
@@ -70,24 +92,25 @@ export abstract class AngularCompilation {
tsconfig: string,
hostOptions: AngularHostOptions,
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
- ): Promise<{
- affectedFiles: ReadonlySet;
- compilerOptions: ng.CompilerOptions;
- referencedFiles: readonly string[];
- externalStylesheets?: ReadonlyMap;
- templateUpdates?: ReadonlyMap;
- componentResourcesDependencies?: ReadonlyMap;
- }>;
-
- abstract emitAffectedFiles(): Iterable | Promise>;
-
- protected abstract collectDiagnostics(
+ ): Promise;
+
+ emitAffectedFiles(): Iterable | Promise> {
+ return [];
+ }
+
+ transformFile?(filename: string, content: string): Promise;
+
+ protected collectDiagnostics?(
modes: DiagnosticModes,
): Iterable | Promise>;
async diagnoseFiles(
modes = DiagnosticModes.All,
): Promise<{ errors?: PartialMessage[]; warnings?: PartialMessage[] }> {
+ if (!this.collectDiagnostics) {
+ return {};
+ }
+
const result: { errors?: PartialMessage[]; warnings?: PartialMessage[] } = {};
// Avoid loading typescript until actually needed.
@@ -95,7 +118,12 @@ export abstract class AngularCompilation {
const typescript = await AngularCompilation.loadTypescript();
await profileAsync('NG_DIAGNOSTICS_TOTAL', async () => {
- for (const diagnostic of await this.collectDiagnostics(modes)) {
+ const diagnostics = await this.collectDiagnostics?.(modes);
+ if (!diagnostics) {
+ return;
+ }
+
+ for (const diagnostic of diagnostics) {
const message = convertTypeScriptDiagnostic(typescript, diagnostic);
if (diagnostic.category === typescript.DiagnosticCategory.Error) {
(result.errors ??= []).push(message);
diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts
new file mode 100644
index 000000000000..a9e93b701e5f
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts
@@ -0,0 +1,109 @@
+/**
+ * @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 type { AngularHostOptions } from '../angular-host';
+import {
+ AngularCompilation,
+ AngularCompilationResult,
+ NoopCompilation,
+ createAngularCompilation,
+} from './index';
+
+describe('AngularCompilation', () => {
+ class CustomTransformCompilation extends AngularCompilation {
+ override async initialize(): Promise {
+ return {
+ compilerOptions: { allowJs: false },
+ referencedFiles: ['/src/main.ts'],
+ };
+ }
+
+ override async transformFile(filename: string, content: string) {
+ if (filename.endsWith('.ts')) {
+ return {
+ contents: content + '\n// transformed',
+ watchFiles: ['/src/dep.ts'],
+ };
+ }
+
+ return null;
+ }
+ }
+
+ it('allows implementing on-demand transformFile without collectDiagnostics', async () => {
+ const compilation = new CustomTransformCompilation();
+ const initResult = await compilation.initialize();
+
+ expect(initResult.referencedFiles).toEqual(['/src/main.ts']);
+ expect(initResult.compilerOptions.allowJs).toBe(false);
+
+ const transformResult = await compilation.transformFile?.(
+ '/src/main.ts',
+ 'console.log("hello");',
+ );
+ expect(transformResult).toEqual({
+ contents: 'console.log("hello");\n// transformed',
+ watchFiles: ['/src/dep.ts'],
+ });
+
+ const diagnostics = await compilation.diagnoseFiles();
+ expect(diagnostics).toEqual({});
+ });
+
+ describe('NoopCompilation', () => {
+ it('initializes with empty referencedFiles and compiler options', async () => {
+ const compilation = new NoopCompilation();
+ const mockHostOptions = {} as AngularHostOptions;
+ const result = await compilation.initialize('tsconfig.json', mockHostOptions, (opts) => ({
+ ...opts,
+ customOption: true,
+ }));
+
+ expect(result.referencedFiles).toEqual([]);
+ expect(result.compilerOptions['customOption']).toBe(true);
+ });
+
+ it('throws when calling collectDiagnostics or emitAffectedFiles', () => {
+ const compilation = new NoopCompilation();
+ expect(() =>
+ (compilation as unknown as { collectDiagnostics(): unknown }).collectDiagnostics(),
+ ).toThrowError('Not available when using noop compilation.');
+ expect(() => compilation.emitAffectedFiles()).toThrowError(
+ 'Not available when using noop compilation.',
+ );
+ });
+ });
+
+ describe('createAngularCompilation', () => {
+ it('creates JitCompilation when mode is "jit"', async () => {
+ const compilation = await createAngularCompilation('jit', true, false);
+ expect(compilation.constructor.name).toBe('JitCompilation');
+ });
+
+ it('creates AotCompilation when mode is "aot"', async () => {
+ const compilation = await createAngularCompilation('aot', true, false);
+ expect(compilation.constructor.name).toBe('AotCompilation');
+ });
+
+ it('creates JitCompilation when mode is boolean true (legacy)', async () => {
+ const compilation = await createAngularCompilation(true, true, false);
+ expect(compilation.constructor.name).toBe('JitCompilation');
+ });
+
+ it('creates AotCompilation when mode is boolean false (legacy)', async () => {
+ const compilation = await createAngularCompilation(false, true, false);
+ expect(compilation.constructor.name).toBe('AotCompilation');
+ });
+
+ it('throws when mode is "transform"', async () => {
+ await expectAsync(createAngularCompilation('transform', true, false)).toBeRejectedWithError(
+ 'Transform compilation mode is not supported.',
+ );
+ });
+ });
+});
diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
index cd4e1cdae7dd..42df6a40e778 100644
--- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
@@ -11,6 +11,7 @@ import assert from 'node:assert';
import { relative } from 'node:path';
import ts from 'typescript';
import { useTypeChecking } from '../../../utils/environment-options';
+import { toPosixPath } from '../../../utils/path';
import { profileAsync, profileSync } from '../../esbuild/profiling';
import {
AngularHostOptions,
@@ -20,7 +21,12 @@ import {
import { replaceBootstrap } from '../transformers/jit-bootstrap-transformer';
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
-import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
+import {
+ AngularCompilation,
+ AngularCompilationResult,
+ DiagnosticModes,
+ EmitFileResult,
+} from './angular-compilation';
import { collectHmrCandidates } from './hmr-candidates';
import { printSourceFileWithMap } from './typescript-printer';
@@ -50,6 +56,7 @@ class AngularCompilationState {
export class AotCompilation extends AngularCompilation {
#state?: AngularCompilationState;
+ readonly #sourceFiles = new Map();
constructor(private readonly browserOnlyBuild: boolean) {
super();
@@ -59,14 +66,7 @@ export class AotCompilation extends AngularCompilation {
tsconfig: string,
hostOptions: AngularHostOptions,
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
- ): Promise<{
- affectedFiles: ReadonlySet;
- compilerOptions: ng.CompilerOptions;
- referencedFiles: readonly string[];
- externalStylesheets?: ReadonlyMap;
- templateUpdates?: ReadonlyMap;
- componentResourcesDependencies?: ReadonlyMap;
- }> {
+ ): Promise {
// Dynamically load the Angular compiler CLI package
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
@@ -99,27 +99,37 @@ export class AotCompilation extends AngularCompilation {
let staleSourceFiles;
let clearPackageJsonCache = false;
- if (hostOptions.modifiedFiles && this.#state) {
+ if (hostOptions.modifiedFiles) {
for (const modifiedFile of hostOptions.modifiedFiles) {
- // Clear package.json cache if a node modules file was modified
- if (!clearPackageJsonCache && modifiedFile.includes('node_modules')) {
- clearPackageJsonCache = true;
- packageJsonCache?.clear();
- }
+ this.#sourceFiles.delete(toPosixPath(modifiedFile));
+
+ if (this.#state) {
+ // Clear package.json cache if a node modules file was modified
+ if (!clearPackageJsonCache && modifiedFile.includes('node_modules')) {
+ clearPackageJsonCache = true;
+ packageJsonCache?.clear();
+ }
- // Collect stale source files for HMR analysis of inline component resources
- if (useHmr) {
- const sourceFile = this.#state.typeScriptProgram.getSourceFile(modifiedFile);
- if (sourceFile) {
- staleSourceFiles ??= new Map();
- staleSourceFiles.set(modifiedFile, sourceFile);
+ // Collect stale source files for HMR analysis of inline component resources
+ if (useHmr) {
+ const sourceFile = this.#state.typeScriptProgram.getSourceFile(modifiedFile);
+ if (sourceFile) {
+ staleSourceFiles ??= new Map();
+ staleSourceFiles.set(modifiedFile, sourceFile);
+ }
}
}
}
}
// Create Angular compiler host
- const host = createAngularCompilerHost(ts, compilerOptions, hostOptions, packageJsonCache);
+ const host = createAngularCompilerHost(
+ ts,
+ compilerOptions,
+ hostOptions,
+ packageJsonCache,
+ this.#sourceFiles,
+ );
// Create the Angular specific program that contains the Angular compiler
const angularProgram = profileSync(
@@ -231,7 +241,6 @@ export class AotCompilation extends AngularCompilation {
);
return {
- affectedFiles,
compilerOptions,
referencedFiles,
externalStylesheets: hostOptions.externalStylesheets,
@@ -240,7 +249,7 @@ export class AotCompilation extends AngularCompilation {
};
}
- *collectDiagnostics(modes: DiagnosticModes): Iterable {
+ protected override *collectDiagnostics(modes: DiagnosticModes): Iterable {
assert(this.#state, 'Angular compilation must be initialized prior to collecting diagnostics.');
const {
affectedFiles,
@@ -313,7 +322,7 @@ export class AotCompilation extends AngularCompilation {
}
}
- emitAffectedFiles(): Iterable {
+ override emitAffectedFiles(): Iterable {
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
const {
affectedFiles,
@@ -454,6 +463,12 @@ export class AotCompilation extends AngularCompilation {
return emittedFiles.values();
}
+
+ override async update(files: Set): Promise {
+ for (const file of files) {
+ this.#sourceFiles.delete(toPosixPath(file));
+ }
+ }
}
function findAffectedFiles(
diff --git a/packages/angular/build/src/tools/angular/compilation/factory.ts b/packages/angular/build/src/tools/angular/compilation/factory.ts
index ebfa7aa7edc4..3944979f1ee5 100644
--- a/packages/angular/build/src/tools/angular/compilation/factory.ts
+++ b/packages/angular/build/src/tools/angular/compilation/factory.ts
@@ -9,26 +9,35 @@
import { useParallelTs } from '../../../utils/environment-options';
import type { AngularCompilation } from './angular-compilation';
+export type AngularCompilationMode = 'aot' | 'jit' | 'transform';
+
/**
* Creates an Angular compilation object that can be used to perform Angular application
- * compilation either for AOT or JIT mode. By default a parallel compilation is created
+ * compilation either for AOT, JIT, or on-demand transform mode. By default a parallel compilation is created
* that uses a Node.js worker thread.
- * @param jit True, for Angular JIT compilation; False, for Angular AOT compilation.
+ * @param mode True or 'jit' for JIT mode; False or 'aot' for AOT compilation; 'transform' for on-demand transformation.
* @param browserOnlyBuild True, for browser only builds; False, for browser and server builds.
+ * @param parallel True to execute compilation in a worker thread.
* @returns An instance of an Angular compilation object.
*/
export async function createAngularCompilation(
- jit: boolean,
+ mode: boolean | AngularCompilationMode,
browserOnlyBuild: boolean,
parallel: boolean = useParallelTs,
): Promise {
+ if (mode === 'transform') {
+ throw new Error('Transform compilation mode is not supported.');
+ }
+
+ const isJit = mode === true || mode === 'jit';
+
if (parallel) {
const { ParallelCompilation } = await import('./parallel-compilation');
- return new ParallelCompilation(jit, browserOnlyBuild);
+ return new ParallelCompilation(isJit, browserOnlyBuild);
}
- if (jit) {
+ if (isJit) {
const { JitCompilation } = await import('./jit-compilation');
return new JitCompilation(browserOnlyBuild);
diff --git a/packages/angular/build/src/tools/angular/compilation/index.ts b/packages/angular/build/src/tools/angular/compilation/index.ts
index d2611e0d156d..213f55cac326 100644
--- a/packages/angular/build/src/tools/angular/compilation/index.ts
+++ b/packages/angular/build/src/tools/angular/compilation/index.ts
@@ -6,6 +6,13 @@
* found in the LICENSE file at https://angular.dev/license
*/
-export { AngularCompilation, DiagnosticModes } from './angular-compilation';
-export { createAngularCompilation } from './factory';
+export {
+ AngularCompilation,
+ type AngularCompilationOptions,
+ type AngularCompilationResult,
+ DiagnosticModes,
+ type EmitFileResult,
+ type FileTransformResult,
+} from './angular-compilation';
+export { createAngularCompilation, type AngularCompilationMode } from './factory';
export { NoopCompilation } from './noop-compilation';
diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts
index 5d920d961ba1..955c90502cb0 100644
--- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts
@@ -9,12 +9,18 @@
import type * as ng from '@angular/compiler-cli';
import assert from 'node:assert';
import ts from 'typescript';
+import { toPosixPath } from '../../../utils/path';
import { profileSync } from '../../esbuild/profiling';
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
import { createJitResourceTransformer } from '../transformers/jit-resource-transformer';
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
-import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
+import {
+ AngularCompilation,
+ AngularCompilationResult,
+ DiagnosticModes,
+ EmitFileResult,
+} from './angular-compilation';
class JitCompilationState {
constructor(
@@ -28,6 +34,7 @@ class JitCompilationState {
export class JitCompilation extends AngularCompilation {
#state?: JitCompilationState;
+ readonly #sourceFiles = new Map();
constructor(private readonly browserOnlyBuild: boolean) {
super();
@@ -37,11 +44,7 @@ export class JitCompilation extends AngularCompilation {
tsconfig: string,
hostOptions: AngularHostOptions,
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
- ): Promise<{
- affectedFiles: ReadonlySet;
- compilerOptions: ng.CompilerOptions;
- referencedFiles: readonly string[];
- }> {
+ ): Promise {
// Dynamically load the Angular compiler CLI package
const { constructorParametersDownlevelTransform } =
await import('@angular/compiler-cli/private/tooling');
@@ -55,8 +58,20 @@ export class JitCompilation extends AngularCompilation {
const compilerOptions =
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
+ if (hostOptions.modifiedFiles) {
+ for (const modifiedFile of hostOptions.modifiedFiles) {
+ this.#sourceFiles.delete(toPosixPath(modifiedFile));
+ }
+ }
+
// Create Angular compiler host
- const host = createAngularCompilerHost(ts, compilerOptions, hostOptions, undefined);
+ const host = createAngularCompilerHost(
+ ts,
+ compilerOptions,
+ hostOptions,
+ undefined,
+ this.#sourceFiles,
+ );
// Create the TypeScript Program
const typeScriptProgram = profileSync('TS_CREATE_PROGRAM', () =>
@@ -69,10 +84,6 @@ export class JitCompilation extends AngularCompilation {
),
);
- const affectedFiles = profileSync('TS_FIND_AFFECTED', () =>
- findAffectedFiles(typeScriptProgram),
- );
-
this.#state = new JitCompilationState(
host,
typeScriptProgram,
@@ -85,10 +96,10 @@ export class JitCompilation extends AngularCompilation {
.getSourceFiles()
.map((sourceFile) => sourceFile.fileName);
- return { affectedFiles, compilerOptions, referencedFiles };
+ return { compilerOptions, referencedFiles };
}
- *collectDiagnostics(modes: DiagnosticModes): Iterable {
+ protected override *collectDiagnostics(modes: DiagnosticModes): Iterable {
assert(this.#state, 'Compilation must be initialized prior to collecting diagnostics.');
const { typeScriptProgram } = this.#state;
@@ -110,7 +121,7 @@ export class JitCompilation extends AngularCompilation {
}
}
- emitAffectedFiles(): Iterable {
+ override emitAffectedFiles(): Iterable {
assert(this.#state, 'Compilation must be initialized prior to emitting files.');
const {
compilerHost,
@@ -156,17 +167,10 @@ export class JitCompilation extends AngularCompilation {
return emittedFiles;
}
-}
-
-function findAffectedFiles(
- builder: ts.EmitAndSemanticDiagnosticsBuilderProgram,
-): Set {
- const affectedFiles = new Set();
- let result;
- while ((result = builder.getSemanticDiagnosticsOfNextAffectedFile())) {
- affectedFiles.add(result.affected as ts.SourceFile);
+ override async update(files: Set): Promise {
+ for (const file of files) {
+ this.#sourceFiles.delete(toPosixPath(file));
+ }
}
-
- return affectedFiles;
}
diff --git a/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts b/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts
index e2c597b4d315..a2bb722d10a8 100644
--- a/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts
@@ -7,33 +7,28 @@
*/
import type * as ng from '@angular/compiler-cli';
-import type ts from 'typescript';
import { AngularHostOptions } from '../angular-host';
-import { AngularCompilation } from './angular-compilation';
+import { AngularCompilation, AngularCompilationResult } from './angular-compilation';
export class NoopCompilation extends AngularCompilation {
async initialize(
tsconfig: string,
hostOptions: AngularHostOptions,
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
- ): Promise<{
- affectedFiles: ReadonlySet;
- compilerOptions: ng.CompilerOptions;
- referencedFiles: readonly string[];
- }> {
+ ): Promise {
// Load the compiler configuration and transform as needed
const { options: originalCompilerOptions } = await this.loadConfiguration(tsconfig);
const compilerOptions =
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
- return { affectedFiles: new Set(), compilerOptions, referencedFiles: [] };
+ return { compilerOptions, referencedFiles: [] };
}
- collectDiagnostics(): never {
+ protected override collectDiagnostics(): never {
throw new Error('Not available when using noop compilation.');
}
- emitAffectedFiles(): never {
+ override emitAffectedFiles(): never {
throw new Error('Not available when using noop compilation.');
}
}
diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts
index a0dd58179ba2..f9a504ee6fc0 100644
--- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts
@@ -10,11 +10,15 @@ import type { CompilerOptions } from '@angular/compiler-cli';
import type { PartialMessage } from 'esbuild';
import { createRequire } from 'node:module';
import { MessageChannel } from 'node:worker_threads';
-import type { SourceFile } from 'typescript';
import { WorkerPool } from '../../../utils/worker-pool';
import { mergeCumulativeDurations } from '../../esbuild/profiling';
import type { AngularHostOptions } from '../angular-host';
-import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
+import {
+ AngularCompilation,
+ AngularCompilationResult,
+ DiagnosticModes,
+ EmitFileResult,
+} from './angular-compilation';
/**
* An Angular compilation which uses a Node.js Worker thread to load and execute
@@ -26,6 +30,7 @@ import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-c
*/
export class ParallelCompilation extends AngularCompilation {
readonly #worker: WorkerPool;
+ #webWorkerChannel?: MessageChannel;
constructor(
private readonly jit: boolean,
@@ -43,16 +48,11 @@ export class ParallelCompilation extends AngularCompilation {
});
}
- override initialize(
+ override async initialize(
tsconfig: string,
hostOptions: AngularHostOptions,
compilerOptionsTransformer?: (compilerOptions: CompilerOptions) => CompilerOptions,
- ): Promise<{
- affectedFiles: ReadonlySet;
- compilerOptions: CompilerOptions;
- referencedFiles: readonly string[];
- externalStylesheets?: ReadonlyMap;
- }> {
+ ): Promise {
const stylesheetChannel = new MessageChannel();
// The request identifier is required because Angular can issue multiple concurrent requests
stylesheetChannel.port1.on(
@@ -65,9 +65,12 @@ export class ParallelCompilation extends AngularCompilation {
},
);
+ this.#webWorkerChannel?.port1.close();
+
// The web worker processing is a synchronous operation and uses shared memory combined with
// the Atomics API to block execution here until a response is received.
const webWorkerChannel = new MessageChannel();
+ this.#webWorkerChannel = webWorkerChannel;
const webWorkerSignal = new Int32Array(new SharedArrayBuffer(4));
webWorkerChannel.port1.on('message', ({ workerFile, containingFile }) => {
try {
@@ -90,31 +93,44 @@ export class ParallelCompilation extends AngularCompilation {
const transformedOptions = compilerOptionsTransformer?.(compilerOptions) ?? compilerOptions;
optionsChannel.port1.postMessage({ transformedOptions });
} catch (error) {
- webWorkerChannel.port1.postMessage({ error });
+ optionsChannel.port1.postMessage({ error });
} finally {
Atomics.store(optionsSignal, 0, 1);
Atomics.notify(optionsSignal, 0);
}
});
- // Execute the initialize function in the worker thread
- return this.#worker.run(
- {
- fileReplacements: hostOptions.fileReplacements,
- tsconfig,
- jit: this.jit,
- browserOnlyBuild: this.browserOnlyBuild,
- stylesheetPort: stylesheetChannel.port2,
- optionsPort: optionsChannel.port2,
- optionsSignal,
- webWorkerPort: webWorkerChannel.port2,
- webWorkerSignal,
- },
- {
- name: 'initialize',
- transferList: [stylesheetChannel.port2, optionsChannel.port2, webWorkerChannel.port2],
- },
- );
+ let success = false;
+ try {
+ // Execute the initialize function in the worker thread
+ const result = await this.#worker.run(
+ {
+ fileReplacements: hostOptions.fileReplacements,
+ tsconfig,
+ jit: this.jit,
+ browserOnlyBuild: this.browserOnlyBuild,
+ stylesheetPort: stylesheetChannel.port2,
+ optionsPort: optionsChannel.port2,
+ optionsSignal,
+ webWorkerPort: webWorkerChannel.port2,
+ webWorkerSignal,
+ },
+ {
+ name: 'initialize',
+ transferList: [stylesheetChannel.port2, optionsChannel.port2, webWorkerChannel.port2],
+ },
+ );
+ success = true;
+
+ return result;
+ } finally {
+ stylesheetChannel.port1.close();
+ optionsChannel.port1.close();
+ if (!success) {
+ this.#webWorkerChannel?.port1.close();
+ this.#webWorkerChannel = undefined;
+ }
+ }
}
/**
@@ -136,8 +152,13 @@ export class ParallelCompilation extends AngularCompilation {
return result;
}
- override emitAffectedFiles(): Promise> {
- return this.#worker.run(undefined, { name: 'emit' });
+ override async emitAffectedFiles(): Promise> {
+ try {
+ return await this.#worker.run(undefined, { name: 'emit' });
+ } finally {
+ this.#webWorkerChannel?.port1.close();
+ this.#webWorkerChannel = undefined;
+ }
}
override update(files: Set): Promise {
@@ -145,6 +166,9 @@ export class ParallelCompilation extends AngularCompilation {
}
override close() {
+ this.#webWorkerChannel?.port1.close();
+ this.#webWorkerChannel = undefined;
+
return this.#worker.destroy();
}
}
diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
index 9e41b7940989..b736e710fa7d 100644
--- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
+++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
@@ -10,9 +10,13 @@ import type { PartialMessage } from 'esbuild';
import assert from 'node:assert';
import { randomUUID } from 'node:crypto';
import { type MessagePort, receiveMessageOnPort } from 'node:worker_threads';
-import { SourceFileCache } from '../../esbuild/angular/source-file-cache';
+import { initializeHash } from '../../../utils/hash';
import { getAndClearCumulativeDurations } from '../../esbuild/profiling';
-import type { AngularCompilation, DiagnosticModes } from './angular-compilation';
+import type {
+ AngularCompilation,
+ AngularCompilationResult,
+ DiagnosticModes,
+} from './angular-compilation';
import { AotCompilation } from './aot-compilation';
import { JitCompilation } from './jit-compilation';
@@ -29,96 +33,122 @@ export interface InitRequest {
}
let compilation: AngularCompilation | undefined;
-
-const sourceFileCache = new SourceFileCache();
-
-export async function initialize(request: InitRequest) {
- compilation ??= request.jit
- ? new JitCompilation(request.browserOnlyBuild)
- : new AotCompilation(request.browserOnlyBuild);
-
- const stylesheetRequests = new Map void, (reason: Error) => void]>();
- request.stylesheetPort.on('message', ({ requestId, value, error }) => {
- if (error) {
- stylesheetRequests.get(requestId)?.[1](error);
- } else {
- stylesheetRequests.get(requestId)?.[0](value);
- }
- });
-
- const {
- compilerOptions,
- referencedFiles,
- externalStylesheets,
- templateUpdates,
- componentResourcesDependencies,
- } = await compilation.initialize(
- request.tsconfig,
- {
- fileReplacements: request.fileReplacements,
- sourceFileCache,
- modifiedFiles: sourceFileCache.modifiedFiles,
- transformStylesheet(data, containingFile, stylesheetFile, order, className) {
- const requestId = randomUUID();
- const resultPromise = new Promise((resolve, reject) =>
- stylesheetRequests.set(requestId, [resolve, reject]),
- );
-
- request.stylesheetPort.postMessage({
- requestId,
- data,
- containingFile,
- stylesheetFile,
- order,
- className,
- });
-
- return resultPromise;
+let activeWebWorkerPort: MessagePort | undefined;
+
+const modifiedFiles = new Set();
+
+export async function initialize(request: InitRequest): Promise {
+ activeWebWorkerPort?.close();
+ activeWebWorkerPort = request.webWorkerPort;
+
+ const currentModifiedFiles = new Set(modifiedFiles);
+ modifiedFiles.clear();
+
+ let success = false;
+ try {
+ await initializeHash();
+ compilation ??= request.jit
+ ? new JitCompilation(request.browserOnlyBuild)
+ : new AotCompilation(request.browserOnlyBuild);
+
+ const stylesheetRequests = new Map<
+ string,
+ [(value: string) => void, (reason: Error) => void]
+ >();
+ request.stylesheetPort.on('message', ({ requestId, value, error }) => {
+ const handlers = stylesheetRequests.get(requestId);
+ if (handlers) {
+ stylesheetRequests.delete(requestId);
+ if (error) {
+ handlers[1](error);
+ } else {
+ handlers[0](value);
+ }
+ }
+ });
+
+ const {
+ compilerOptions,
+ referencedFiles,
+ externalStylesheets,
+ templateUpdates,
+ componentResourcesDependencies,
+ } = await compilation.initialize(
+ request.tsconfig,
+ {
+ fileReplacements: request.fileReplacements,
+ modifiedFiles: currentModifiedFiles,
+ transformStylesheet(data, containingFile, stylesheetFile, order, className) {
+ const requestId = randomUUID();
+ const resultPromise = new Promise((resolve, reject) =>
+ stylesheetRequests.set(requestId, [resolve, reject]),
+ );
+
+ request.stylesheetPort.postMessage({
+ requestId,
+ data,
+ containingFile,
+ stylesheetFile,
+ order,
+ className,
+ });
+
+ return resultPromise;
+ },
+ processWebWorker(workerFile, containingFile) {
+ Atomics.store(request.webWorkerSignal, 0, 0);
+ request.webWorkerPort.postMessage({ workerFile, containingFile });
+
+ Atomics.wait(request.webWorkerSignal, 0, 0);
+ const result = receiveMessageOnPort(request.webWorkerPort)?.message;
+
+ if (result?.error) {
+ throw result.error;
+ }
+
+ return result?.workerCodeFile ?? workerFile;
+ },
},
- processWebWorker(workerFile, containingFile) {
- Atomics.store(request.webWorkerSignal, 0, 0);
- request.webWorkerPort.postMessage({ workerFile, containingFile });
+ (compilerOptions) => {
+ Atomics.store(request.optionsSignal, 0, 0);
+ request.optionsPort.postMessage(compilerOptions);
- Atomics.wait(request.webWorkerSignal, 0, 0);
- const result = receiveMessageOnPort(request.webWorkerPort)?.message;
+ Atomics.wait(request.optionsSignal, 0, 0);
+ const result = receiveMessageOnPort(request.optionsPort)?.message;
if (result?.error) {
throw result.error;
}
- return result?.workerCodeFile ?? workerFile;
+ return result?.transformedOptions ?? compilerOptions;
},
- },
- (compilerOptions) => {
- Atomics.store(request.optionsSignal, 0, 0);
- request.optionsPort.postMessage(compilerOptions);
-
- Atomics.wait(request.optionsSignal, 0, 0);
- const result = receiveMessageOnPort(request.optionsPort)?.message;
-
- if (result?.error) {
- throw result.error;
- }
-
- return result?.transformedOptions ?? compilerOptions;
- },
- );
-
- return {
- externalStylesheets,
- templateUpdates,
- referencedFiles,
- // TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
- compilerOptions: {
- allowJs: compilerOptions.allowJs,
- isolatedModules: compilerOptions.isolatedModules,
- sourceMap: compilerOptions.sourceMap,
- inlineSourceMap: compilerOptions.inlineSourceMap,
- _useTypeScriptTranspilation: compilerOptions['_useTypeScriptTranspilation'] as
- boolean | undefined,
- },
- componentResourcesDependencies,
- };
+ );
+
+ success = true;
+
+ return {
+ externalStylesheets,
+ templateUpdates,
+ referencedFiles,
+ // TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
+ compilerOptions: {
+ allowJs: compilerOptions.allowJs,
+ isolatedModules: compilerOptions.isolatedModules,
+ sourceMap: compilerOptions.sourceMap,
+ inlineSourceMap: compilerOptions.inlineSourceMap,
+ _useTypeScriptTranspilation: compilerOptions['_useTypeScriptTranspilation'] as
+ boolean | undefined,
+ },
+ componentResourcesDependencies,
+ };
+ } finally {
+ request.stylesheetPort.close();
+ request.optionsPort.close();
+ if (!success) {
+ activeWebWorkerPort?.close();
+ activeWebWorkerPort = undefined;
+ }
+ }
}
export async function diagnose(modes: DiagnosticModes): Promise<{
@@ -140,11 +170,19 @@ export async function diagnose(modes: DiagnosticModes): Promise<{
export async function emit() {
assert(compilation);
- const files = await compilation.emitAffectedFiles();
+ try {
+ const files = await compilation.emitAffectedFiles();
- return [...files];
+ return [...files];
+ } finally {
+ activeWebWorkerPort?.close();
+ activeWebWorkerPort = undefined;
+ }
}
-export function update(files: Set): void {
- sourceFileCache.invalidate(files);
+export async function update(files: Set): Promise {
+ for (const file of files) {
+ modifiedFiles.add(file);
+ }
+ await compilation?.update?.(files);
}
diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts
index 467a1332cd97..26133a6b5611 100644
--- a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts
+++ b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts
@@ -6,19 +6,13 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import type { EncodedSourceMap } from '@ampproject/remapping';
-import remapping from '@ampproject/remapping';
import { ConsoleLogger, LogLevel } from '@angular/compiler-cli';
-import type { DeclarationScope } from '@angular/compiler-cli/linker';
-import { FileLinker, LinkerEnvironment, needsLinking } from '@angular/compiler-cli/linker';
+import { type DeclarationScope, FileLinker, LinkerEnvironment } from '@angular/compiler-cli/linker';
import type {
AbsoluteFsPath,
ReadonlyFileSystem,
} from '@angular/compiler-cli/src/ngtsc/file_system';
-import type { CallExpression, Node } from '@oxc-project/types';
-import MagicString from 'magic-string';
-import { parseSync, visitorKeys } from 'oxc-parser';
-import { loadInputSourceMap } from '../../../utils/source-map';
+import type { CallExpression } from '@oxc-project/types';
import { OxcAstHost } from './oxc-ast-host';
import { StringAstFactory } from './string-ast-factory';
@@ -51,136 +45,52 @@ const noopFileSystem: ReadonlyFileSystem = {
relative: (_from: string, to: string) => to,
} as unknown as ReadonlyFileSystem;
-const SHARED_LOGGER = new ConsoleLogger(LogLevel.info);
-
-const SHARED_AST_HOST = new OxcAstHost();
-const SHARED_DECLARATION_SCOPE = new InlineDeclarationScope();
-
-/**
- * Recursively traverses ESTree AST nodes with subtree pruning.
- * When `onCallExpression` returns `true` for a linked `CallExpression`,
- * child traversal into `callee` and `arguments` is skipped.
- *
- * Why subtree pruning is safe for the linker:
- * - Angular partial declarations (`ɵɵngDeclareComponent`, `ɵɵngDeclareDirective`,
- * etc.) are never nested inside each other.
- * - Once a declaration `CallExpression` is linked and replaced, there can never be
- * another partial declaration within its metadata argument object. Pruning its
- * subtree avoids traversing hundreds of unnecessary metadata argument nodes per
- * component.
- */
-function visitNode(
- node: Node | Node[] | null | undefined,
- onCallExpression: (node: CallExpression) => boolean,
-): void {
- if (node === null || node === undefined || typeof node !== 'object') {
- return;
- }
-
- if (Array.isArray(node)) {
- for (let i = 0; i < node.length; i++) {
- visitNode(node[i], onCallExpression);
- }
-
- return;
- }
-
- const nodeType = node.type;
- if (!nodeType) {
- return;
- }
-
- if (nodeType === 'CallExpression') {
- if (onCallExpression(node)) {
- // Subtree pruning: partial declarations cannot be nested, so skip child traversal.
- return;
- }
- }
-
- const keys = visitorKeys[nodeType];
- if (keys) {
- for (let i = 0; i < keys.length; i++) {
- const child = (node as unknown as Record)[keys[i]];
- if (child !== undefined && child !== null) {
- visitNode(child, onCallExpression);
- }
- }
- }
-}
-
-export interface OxcLinkerOptions {
- sourcemap?: boolean;
- jit?: boolean;
- skipCheck?: boolean;
-}
+let SHARED_LOGGER: ConsoleLogger;
+let SHARED_AST_HOST: OxcAstHost;
+let SHARED_DECLARATION_SCOPE: InlineDeclarationScope;
/**
- * Executes Angular partial declaration linking on the specified JavaScript file
- * using `oxc-parser` and `magic-string`.
- *
- * @param filename The full path to the file.
- * @param code The source code content.
- * @param options Linker options (sourcemap, jit, skipCheck).
- * @returns An object containing the transformed code and optional source map.
+ * Manages Angular partial declaration linking using Oxc AST nodes.
*/
-export function linkWithOxc(filename: string, code: string, options: OxcLinkerOptions = {}) {
- if (!options.skipCheck && !needsLinking(filename, code)) {
- return { code, map: undefined };
+export class OxcLinker {
+ readonly #fileLinker: FileLinker;
+
+ constructor(filename: string, code: string, jit = false) {
+ SHARED_LOGGER ??= new ConsoleLogger(LogLevel.info);
+ SHARED_AST_HOST ??= new OxcAstHost();
+ SHARED_DECLARATION_SCOPE ??= new InlineDeclarationScope();
+
+ const astFactory = new StringAstFactory(code);
+ const linkerEnvironment = LinkerEnvironment.create(
+ noopFileSystem,
+ SHARED_LOGGER,
+ SHARED_AST_HOST,
+ astFactory,
+ { linkerJitMode: jit, sourceMapping: false },
+ );
+
+ this.#fileLinker = new FileLinker(linkerEnvironment, filename as AbsoluteFsPath, code);
}
- const astFactory = new StringAstFactory(code);
-
- const linkerEnvironment = LinkerEnvironment.create(
- noopFileSystem,
- SHARED_LOGGER,
- SHARED_AST_HOST,
- astFactory,
- { linkerJitMode: options.jit ?? false, sourceMapping: false },
- );
-
- const fileLinker = new FileLinker(linkerEnvironment, filename as AbsoluteFsPath, code);
- const { program } = parseSync(filename, code, { range: true });
-
- let s: MagicString | undefined;
- let hasLinked = false;
-
- visitNode(program, (node) => {
+ /**
+ * Attempts to link an Angular partial declaration CallExpression.
+ *
+ * @param node The CallExpression AST node to check and link.
+ * @returns The linked code string if the node is a partial declaration, or undefined otherwise.
+ */
+ linkCallExpression(node: CallExpression): string | undefined {
const calleeName = SHARED_AST_HOST.getSymbolName(node.callee);
- if (calleeName && fileLinker.isPartialDeclaration(calleeName)) {
- const args = SHARED_AST_HOST.parseArguments(node);
- const linkedCode = fileLinker.linkPartialDeclaration(
- calleeName,
- args,
- SHARED_DECLARATION_SCOPE,
- );
-
- s ??= new MagicString(code);
- s.overwrite(node.start, node.end, linkedCode as string);
- hasLinked = true;
-
- return true;
+ if (!calleeName || !this.#fileLinker.isPartialDeclaration(calleeName)) {
+ return undefined;
}
- return false;
- });
+ const args = SHARED_AST_HOST.parseArguments(node);
+ const linkedCode = this.#fileLinker.linkPartialDeclaration(
+ calleeName,
+ args,
+ SHARED_DECLARATION_SCOPE,
+ );
- if (!hasLinked || !s) {
- return { code, map: undefined };
+ return linkedCode as string;
}
-
- let map: string | undefined;
- if (options.sourcemap) {
- const rawMap = s.generateMap({ hires: true, source: filename });
- const inputMap = loadInputSourceMap(filename, code);
- if (inputMap) {
- map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString();
- } else {
- map = rawMap.toString();
- }
- }
-
- return {
- code: s.toString(),
- map,
- };
}
diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts
index 148c67f9dce3..fa62c1c5ece1 100644
--- a/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts
+++ b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts
@@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import { linkWithOxc } from './oxc-linker';
+import { transform } from '../../oxc/oxc-transform';
-describe('linkWithOxc', () => {
+describe('oxc-linker', () => {
it('should not modify code that does not need linking', () => {
const input = 'const x = 1;';
- const result = linkWithOxc('test.js', input);
+ const result = transform('test.js', input, { link: true, advancedOptimizations: false });
expect(result.code).toBe(input);
expect(result.map).toBeUndefined();
});
@@ -29,7 +29,7 @@ describe('linkWithOxc', () => {
});
`;
- const result = linkWithOxc('test.js', input);
+ const result = transform('test.js', input, { link: true, advancedOptimizations: false });
expect(result.code).toContain('i0.ɵɵdefineDirective');
expect(result.code).not.toContain('i0.ɵɵngDeclareDirective');
});
@@ -49,8 +49,31 @@ describe('linkWithOxc', () => {
});
`;
- const result = linkWithOxc('test.js', input);
+ const result = transform('test.js', input, { link: true, advancedOptimizations: false });
expect(result.code).toContain('i0.ɵɵdefineComponent');
expect(result.code).not.toContain('i0.ɵɵngDeclareComponent');
});
+
+ it('should generate a decoded sourcemap when sourcemap option is enabled', () => {
+ const input = `
+ import * as i0 from "@angular/core";
+ export class MyDirective {}
+ MyDirective.ɵdir = i0.ɵɵngDeclareDirective({
+ minVersion: "12.0.0",
+ version: "14.0.0",
+ ngImport: i0,
+ type: MyDirective,
+ selector: "[my-dir]"
+ });
+ `;
+
+ const result = transform('test.js', input, {
+ link: true,
+ advancedOptimizations: false,
+ sourcemap: true,
+ });
+ expect(result.map).toBeDefined();
+ expect(result.map?.version).toBe(3);
+ expect(result.map?.sources).toContain('test.js');
+ });
});
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 131547f78366..b0ff0593cecc 100644
--- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
@@ -18,10 +18,10 @@ import type {
PluginBuild,
} from 'esbuild';
import assert from 'node:assert';
-import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import * as path from 'node:path';
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
+import { calculateHash, initializeHash } from '../../../utils/hash';
import { AngularHostOptions } from '../../angular/angular-host';
import { AngularCompilation, DiagnosticModes, NoopCompilation } from '../../angular/compilation';
import { type PersistentCacheStore, createPersistentCacheStore } from '../cache';
@@ -149,6 +149,7 @@ export function createCompilerPlugin(
// eslint-disable-next-line max-lines-per-function
build.onStart(async () => {
+ await initializeHash();
angularCompilationContext.markAsInProgress();
const result: OnStartResult = {
@@ -186,7 +187,6 @@ export function createCompilerPlugin(
const hostOptions: AngularHostOptions = {
fileReplacements: pluginOptions.fileReplacements,
modifiedFiles,
- sourceFileCache: pluginOptions.sourceFileCache,
async transformStylesheet(data, containingFile, stylesheetFile, order, className) {
let stylesheetResult;
let resultSource = stylesheetFile ?? containingFile;
@@ -205,11 +205,7 @@ export function createCompilerPlugin(
// invalid the output and force a full page reload for HMR cases. The containing file and order
// of the style within the containing file is used.
pluginOptions.externalRuntimeStyles
- ? createHash('sha256')
- .update(containingFile)
- .update((order ?? 0).toString())
- .update(className ?? '')
- .digest('hex')
+ ? calculateHash(`${containingFile}${order ?? 0}${className ?? ''}`)
: undefined,
);
// Adjust result source for inline styles.
@@ -365,24 +361,26 @@ export function createCompilerPlugin(
}
}
- // Update TypeScript file output cache for all affected files
- try {
- await profileAsync('NG_EMIT_TS', async () => {
- for (const { filename, contents } of await compilation.emitAffectedFiles()) {
- typeScriptFileCache.set(path.normalize(filename), contents);
- }
- });
- } catch (error) {
- (result.errors ??= []).push({
- text: 'Angular compilation emit failed.',
- location: null,
- notes: [
- {
- text: error instanceof Error ? (error.stack ?? error.message) : `${error}`,
- location: null,
- },
- ],
- });
+ // Update TypeScript file output cache for all affected files if not using on-demand transforms
+ if (!compilation.transformFile) {
+ try {
+ await profileAsync('NG_EMIT_TS', async () => {
+ for (const { filename, contents } of await compilation.emitAffectedFiles()) {
+ typeScriptFileCache.set(path.normalize(filename), contents);
+ }
+ });
+ } catch (error) {
+ (result.errors ??= []).push({
+ text: 'Angular compilation emit failed.',
+ location: null,
+ notes: [
+ {
+ text: error instanceof Error ? (error.stack ?? error.message) : `${error}`,
+ location: null,
+ },
+ ],
+ });
+ }
}
const diagnostics = await compilation.diagnoseFiles(
@@ -437,6 +435,35 @@ export function createCompilerPlugin(
// cache is later stored to disk, then the options that affect transform output
// would need to be added to the key as well as a check for any change of content.
let contents = typeScriptFileCache.get(request);
+ let directContents: string | undefined;
+
+ if (contents === undefined && compilation.transformFile) {
+ try {
+ directContents = await readFile(request, 'utf-8');
+ const transformResult = await compilation.transformFile(request, directContents);
+ if (transformResult) {
+ contents = transformResult.contents;
+ if (transformResult.watchFiles) {
+ referencedFileTracker.add(request, transformResult.watchFiles);
+ }
+ }
+ } catch (error) {
+ return {
+ errors: [
+ {
+ text: 'Angular compilation transform failed.',
+ location: { file: request },
+ notes: [
+ {
+ text: error instanceof Error ? (error.stack ?? error.message) : `${error}`,
+ location: null,
+ },
+ ],
+ },
+ ],
+ };
+ }
+ }
if (contents === undefined) {
// If the Angular compilation had errors the file may not have been emitted.
@@ -455,11 +482,11 @@ export function createCompilerPlugin(
// Evaluate whether the file requires the Angular compiler transpilation.
// If not, issue a warning but allow bundler to process the file (no type-checking).
- const directContents = await readFile(request, 'utf-8');
+ directContents ??= await readFile(request, 'utf-8');
if (!requiresAngularCompiler(directContents)) {
return {
warnings: [createMissingFileDiagnostic(request, args.path, diangosticRoot, false)],
- contents,
+ contents: directContents,
loader: 'ts',
resolveDir: path.dirname(request),
};
diff --git a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts
index 79008d140729..dd1b3b704150 100644
--- a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts
@@ -7,11 +7,12 @@
*/
import assert from 'node:assert';
-import { createHash } from 'node:crypto';
import path from 'node:path';
+import { createContentHash } from '../../../utils/hash';
import { BundleContextResult, BundlerContext } from '../bundler-context';
import { type BuildOutputFile, BuildOutputFileType } from '../bundler-files';
import { MemoryCache } from '../cache';
+import { MemoryLoadResultCache } from '../load-result-cache';
import {
BundleStylesheetOptions,
createStylesheetBundleOptions,
@@ -30,11 +31,12 @@ export type ComponentStylesheetResult = BundleContextResult & {
export class ComponentStylesheetBundler {
readonly #fileContexts = new MemoryCache();
readonly #inlineContexts = new MemoryCache();
+ readonly #loadCache = new MemoryLoadResultCache();
/**
- *
* @param options An object containing the stylesheet bundling options.
- * @param cache A load result cache to use when bundling.
+ * @param defaultInlineLanguage The default language to use for inline component styles.
+ * @param incremental True if incremental watch mode is enabled.
*/
constructor(
private readonly options: BundleStylesheetOptions,
@@ -54,29 +56,38 @@ export class ComponentStylesheetBundler {
externalId?: string | boolean,
direct?: boolean,
): Promise {
- const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
- return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
- const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
- if (externalId) {
- assert(
- typeof externalId === 'string',
- 'Initial external component stylesheets must have a string identifier',
- );
-
- buildOptions.entryPoints = { [externalId]: entry };
- buildOptions.entryNames = '[name]';
- delete buildOptions.publicPath;
- } else {
- buildOptions.entryPoints = [entry];
- }
+ entry = path.normalize(entry);
- // Angular encapsulation does not support nesting
- // See: https://github.com/angular/angular/issues/58996
- buildOptions.supported ??= {};
- buildOptions.supported['nesting'] = false;
+ const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
+ return new BundlerContext(
+ this.options.workspaceRoot,
+ this.incremental,
+ (loadCache) => {
+ const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
+ if (externalId) {
+ assert(
+ typeof externalId === 'string',
+ 'Initial external component stylesheets must have a string identifier',
+ );
+
+ buildOptions.entryPoints = { [externalId]: entry };
+ buildOptions.entryNames = '[name]';
+ delete buildOptions.publicPath;
+ } else {
+ buildOptions.entryPoints = [entry];
+ }
- return buildOptions;
- });
+ // Angular encapsulation does not support nesting
+ // See: https://github.com/angular/angular/issues/58996
+ buildOptions.supported ??= {};
+ buildOptions.supported['nesting'] = false;
+
+ return buildOptions;
+ },
+ /* useContext */ false,
+ /* initialFilter */ undefined,
+ this.#loadCache,
+ );
});
return this.extractResult(
@@ -101,60 +112,68 @@ export class ComponentStylesheetBundler {
language = this.defaultInlineLanguage,
externalId?: string,
): Promise {
+ filename = path.normalize(filename);
+
// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
// to the actual stylesheet file path.
- // TODO: Consider xxhash instead for hashing
- const id = createHash('sha256')
- .update(data)
- .update(externalId ?? '')
- .digest('hex');
+ const hasher = createContentHash();
+ hasher.update(data);
+ hasher.update(externalId ?? '');
+ const id = hasher.digest();
const entry = [language, id, filename].join(';');
const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {
const namespace = 'angular:styles/component';
- return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
- const buildOptions = createStylesheetBundleOptions(this.options, loadCache, {
- [entry]: data,
- });
- if (externalId) {
- buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
- buildOptions.entryNames = '[name]';
- delete buildOptions.publicPath;
- } else {
- buildOptions.entryPoints = [`${namespace};${entry}`];
- }
+ return new BundlerContext(
+ this.options.workspaceRoot,
+ this.incremental,
+ (loadCache) => {
+ const buildOptions = createStylesheetBundleOptions(this.options, loadCache, {
+ [entry]: data,
+ });
+ if (externalId) {
+ buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
+ buildOptions.entryNames = '[name]';
+ delete buildOptions.publicPath;
+ } else {
+ buildOptions.entryPoints = [`${namespace};${entry}`];
+ }
- // Angular encapsulation does not support nesting
- // See: https://github.com/angular/angular/issues/58996
- buildOptions.supported ??= {};
- buildOptions.supported['nesting'] = false;
-
- buildOptions.plugins.push({
- name: 'angular-component-styles',
- setup(build) {
- build.onResolve({ filter: /^angular:styles\/component;/ }, (args) => {
- if (args.kind !== 'entry-point') {
- return null;
- }
-
- return {
- path: entry,
- namespace,
- };
- });
- build.onLoad({ filter: /^css;/, namespace }, () => {
- return {
- contents: data,
- loader: 'css',
- resolveDir: path.dirname(filename),
- };
- });
- },
- });
-
- return buildOptions;
- });
+ // Angular encapsulation does not support nesting
+ // See: https://github.com/angular/angular/issues/58996
+ buildOptions.supported ??= {};
+ buildOptions.supported['nesting'] = false;
+
+ buildOptions.plugins.push({
+ name: 'angular-component-styles',
+ setup(build) {
+ build.onResolve({ filter: /^angular:styles\/component;/ }, (args) => {
+ if (args.kind !== 'entry-point') {
+ return null;
+ }
+
+ return {
+ path: entry,
+ namespace,
+ };
+ });
+ build.onLoad({ filter: /^css;/, namespace }, () => {
+ return {
+ contents: data,
+ loader: 'css',
+ resolveDir: path.dirname(filename),
+ };
+ });
+ },
+ });
+
+ return buildOptions;
+ },
+ /* useContext */ false,
+ /* initialFilter */ undefined,
+ this.#loadCache,
+ );
});
// Extract the result of the bundling from the output files
@@ -171,12 +190,20 @@ export class ComponentStylesheetBundler {
* @param files The group of files that have been modified
* @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined.
*/
- invalidate(files: Iterable): string[] | undefined {
+ invalidate(files: Iterable | ReadonlySet): string[] | undefined {
if (!this.incremental) {
return;
}
- const normalizedFiles = [...files].map(path.normalize);
+ const normalizedFiles = new Set();
+ for (const file of files) {
+ const normalized = path.normalize(file);
+ normalizedFiles.add(normalized);
+ if (!path.isAbsolute(normalized)) {
+ normalizedFiles.add(path.normalize(path.join(this.options.workspaceRoot, normalized)));
+ }
+ }
+
let entries: string[] | undefined;
for (const [entry, bundler] of this.#fileContexts.entries()) {
@@ -185,8 +212,17 @@ export class ComponentStylesheetBundler {
entries.push(entry);
}
}
- for (const bundler of this.#inlineContexts.values()) {
- bundler.invalidate(normalizedFiles);
+ for (const [entry, bundler] of this.#inlineContexts.entries()) {
+ // Entry is format: [language, id, filename].join(';')
+ const firstSemi = entry.indexOf(';');
+ const secondSemi = firstSemi !== -1 ? entry.indexOf(';', firstSemi + 1) : -1;
+ const filename = secondSemi !== -1 ? entry.slice(secondSemi + 1) : '';
+ if (filename && normalizedFiles.has(path.normalize(filename))) {
+ this.#inlineContexts.delete(entry);
+ void bundler.dispose();
+ } else {
+ bundler.invalidate(normalizedFiles);
+ }
}
return entries;
@@ -205,6 +241,7 @@ export class ComponentStylesheetBundler {
const contexts = [...this.#fileContexts.values(), ...this.#inlineContexts.values()];
this.#fileContexts.clear();
this.#inlineContexts.clear();
+ this.#loadCache.clear();
await Promise.allSettled(contexts.map((context) => context.dispose()));
}
@@ -259,14 +296,18 @@ export class ComponentStylesheetBundler {
}
}
- const metafile = result.metafile;
- // Remove entryPoint fields from outputs to prevent the internal component styles from being
- // treated as initial files. Also mark the entry as a component resource for stat reporting.
- Object.values(metafile.outputs).forEach((output) => {
- delete output.entryPoint;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (output as any)['ng-component'] = true;
- });
+ // Clone metafile to prevent mutation of the cached result by downstream plugins
+ const metafile = {
+ inputs: { ...result.metafile.inputs },
+ outputs: Object.fromEntries(
+ Object.entries(result.metafile.outputs).map(([key, output]) => {
+ const cloned = { ...output, ['ng-component']: true };
+ delete cloned.entryPoint;
+
+ return [key, cloned];
+ }),
+ ),
+ };
return {
errors,
@@ -276,6 +317,7 @@ export class ComponentStylesheetBundler {
metafile,
referencedFiles,
externalImports: result.externalImports,
+ platform: result.platform,
initialFiles: new Map(),
};
}
diff --git a/packages/angular/build/src/tools/esbuild/angular/source-file-cache.ts b/packages/angular/build/src/tools/esbuild/angular/source-file-cache.ts
index a408650a4f4f..136fbb651ba2 100644
--- a/packages/angular/build/src/tools/esbuild/angular/source-file-cache.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/source-file-cache.ts
@@ -6,32 +6,24 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import { platform } from 'node:os';
import * as path from 'node:path';
-import type ts from 'typescript';
import { MemoryLoadResultCache } from '../load-result-cache';
-const USING_WINDOWS = platform() === 'win32';
-const WINDOWS_SEP_REGEXP = new RegExp(`\\${path.win32.sep}`, 'g');
-
-export class SourceFileCache extends Map {
+export class SourceFileCache {
readonly modifiedFiles = new Set();
readonly typeScriptFileCache = new Map();
readonly loadResultCache = new MemoryLoadResultCache();
referencedFiles?: readonly string[];
- constructor(readonly persistentCachePath?: string) {
- super();
- }
+ constructor(readonly persistentCachePath?: string) {}
/**
* Releases all cached content. The cached data is only needed for incremental
* rebuilds and can include the emitted contents of every TypeScript file in the
* program. The cache is repopulated if a build is performed after this is called.
*/
- override clear(): void {
- super.clear();
+ clear(): void {
this.modifiedFiles.clear();
this.typeScriptFileCache.clear();
this.loadResultCache.clear();
@@ -50,13 +42,6 @@ export class SourceFileCache extends Map {
file = path.normalize(file);
invalid = this.loadResultCache.invalidate(file) || invalid;
invalid = extraWatchFiles.has(file) || invalid;
-
- // Normalize separators to allow matching TypeScript Host paths
- if (USING_WINDOWS) {
- file = file.replace(WINDOWS_SEP_REGEXP, path.posix.sep);
- }
-
- invalid = this.delete(file) || invalid;
this.modifiedFiles.add(file);
}
diff --git a/packages/angular/build/src/tools/esbuild/angular/source-file-cache_spec.ts b/packages/angular/build/src/tools/esbuild/angular/source-file-cache_spec.ts
new file mode 100644
index 000000000000..1a3b2c402618
--- /dev/null
+++ b/packages/angular/build/src/tools/esbuild/angular/source-file-cache_spec.ts
@@ -0,0 +1,34 @@
+/**
+ * @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 { SourceFileCache } from './source-file-cache';
+
+describe('SourceFileCache', () => {
+ let cache: SourceFileCache;
+
+ beforeEach(() => {
+ cache = new SourceFileCache();
+ });
+
+ it('should clean up all caches on clear()', async () => {
+ cache.typeScriptFileCache.set('/test/app.component.ts', 'console.log("test");');
+ cache.modifiedFiles.add('/test/app.component.ts');
+ cache.referencedFiles = ['/test/app.component.ts'];
+ await cache.loadResultCache.put('file:/test/app.component.ts', {
+ contents: 'test',
+ loader: 'js',
+ });
+
+ cache.clear();
+
+ expect(cache.typeScriptFileCache.size).toBe(0);
+ expect(cache.modifiedFiles.size).toBe(0);
+ expect(cache.referencedFiles).toBeUndefined();
+ expect(cache.loadResultCache.get('file:/test/app.component.ts')).toBeUndefined();
+ });
+});
diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts
index 37ff846c7400..afb03d436d29 100644
--- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts
+++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts
@@ -8,18 +8,17 @@
import type { BuildOptions, Plugin } from 'esbuild';
import assert from 'node:assert';
-import { createHash } from 'node:crypto';
import { extname, relative } from 'node:path';
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import { Platform } from '../../builders/application/schema';
import { allowMangle } from '../../utils/environment-options';
+import { calculateHash } from '../../utils/hash';
import { toPosixPath } from '../../utils/path';
import {
SERVER_APP_ENGINE_MANIFEST_FILENAME,
SERVER_APP_MANIFEST_FILENAME,
SERVER_GENERATED_EXTERNALS,
} from '../../utils/server-rendering/manifest';
-import { AngularCompilation, NoopCompilation } from '../angular/compilation';
import { AngularCompilationContext } from './angular/compilation-state';
import { createCompilerPlugin } from './angular/compiler-plugin';
import { ComponentStylesheetBundler } from './angular/component-stylesheets';
@@ -71,6 +70,15 @@ export function createBrowserCodeBundleOptions(
supported: getFeatureSupport(zoneless),
};
+ if (options.disableCodeSplitting) {
+ // Splitting emits shared chunks that are read across chunk boundaries as live ESM bindings,
+ // which the unit-test runners' module loading does not reliably preserve.
+ buildOptions.splitting = false;
+ // In unit test builds, package.json "sideEffects": false annotations can cause esbuild
+ // to incorrectly elide statically-referenced barrel module bodies across multiple entry points.
+ buildOptions.ignoreAnnotations = true;
+ }
+
buildOptions.plugins ??= [];
buildOptions.plugins.push(
createWasmPlugin({ allowAsync: zoneless, cache: loadCache }),
@@ -566,7 +574,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
'',
);
- footer = { js: `/**i18n:${createHash('sha256').update(i18nHash).digest('hex')}*/` };
+ footer = { js: `/**i18n:${calculateHash(i18nHash)}*/` };
}
// Core conditions that are always included
@@ -689,20 +697,12 @@ function getEsBuildCommonPolyfillsOptions(
}
// Add Angular's global locale data if i18n options are present.
- // Locale data should go first so that project provided polyfill code can augment if needed.
let needLocaleDataPlugin = false;
if (i18nOptions.shouldInline) {
// Remove localize polyfill when i18n inline transformation have been applied to all the packages.
polyfills = polyfills.filter((path) => !path.startsWith('@angular/localize'));
-
- // Add locale data for all active locales
- // TODO: Inject each individually within the inlining process itself
- for (const locale of i18nOptions.inlineLocales) {
- polyfills.unshift(`angular:locale/data:${locale}`);
- }
- needLocaleDataPlugin = true;
} else if (i18nOptions.hasDefinedSourceLocale) {
- // When not inlining and a source local is present, use the source locale data directly
+ // When not inlining and a source locale is present, use the source locale data directly
polyfills.unshift(`angular:locale/data:${i18nOptions.sourceLocale}`);
needLocaleDataPlugin = true;
}
@@ -710,7 +710,7 @@ function getEsBuildCommonPolyfillsOptions(
buildOptions.plugins.unshift(createAngularLocaleDataPlugin());
}
- if (polyfills.length === 0) {
+ if (polyfills.length === 0 && !i18nOptions.shouldInline && !i18nOptions.hasDefinedSourceLocale) {
return;
}
diff --git a/packages/angular/build/src/tools/esbuild/budget-stats.ts b/packages/angular/build/src/tools/esbuild/budget-stats.ts
index a9c32778b3db..a14d9a24f1b6 100644
--- a/packages/angular/build/src/tools/esbuild/budget-stats.ts
+++ b/packages/angular/build/src/tools/esbuild/budget-stats.ts
@@ -30,12 +30,12 @@ export function generateBudgetStats(
};
for (const { path: file, size, type } of outputFiles) {
- if (!file.endsWith('.js') && !file.endsWith('.css')) {
+ // Exclude server bundles
+ if (type === BuildOutputFileType.ServerApplication || type === BuildOutputFileType.ServerRoot) {
continue;
}
- // Exclude server bundles
- if (type === BuildOutputFileType.ServerApplication || type === BuildOutputFileType.ServerRoot) {
+ if (!file.endsWith('.js') && !file.endsWith('.css')) {
continue;
}
diff --git a/packages/angular/build/src/tools/esbuild/bundler-context.ts b/packages/angular/build/src/tools/esbuild/bundler-context.ts
index d3f3ca567a0f..b335a1160ba6 100644
--- a/packages/angular/build/src/tools/esbuild/bundler-context.ts
+++ b/packages/angular/build/src/tools/esbuild/bundler-context.ts
@@ -17,7 +17,7 @@ import {
context,
} from 'esbuild';
import assert from 'node:assert';
-import { basename, extname, join, relative } from 'node:path';
+import { basename, extname, isAbsolute, join, normalize, relative } from 'node:path';
import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest';
import {
type BuildOutputFile,
@@ -33,11 +33,27 @@ export type BundleContextResult =
errors: undefined;
warnings: Message[];
metafile: Metafile;
+ platform: 'browser' | 'server';
+ outputFiles: BuildOutputFile[];
+ initialFiles: Map;
+ externalImports: Set;
+ externalConfiguration?: string[];
+ };
+
+export type BundleMergedContextResult =
+ | { errors: Message[]; warnings: Message[] }
+ | {
+ errors: undefined;
+ warnings: Message[];
+ metafiles: {
+ browser: Metafile;
+ server: Metafile;
+ };
outputFiles: BuildOutputFile[];
initialFiles: Map;
externalImports: {
- server?: Set;
- browser?: Set;
+ server: Set;
+ browser: Set;
};
externalConfiguration?: string[];
};
@@ -63,16 +79,18 @@ export class BundlerContext {
#disposed = false;
#optionsFactory: BundlerOptionsFactory;
#shouldCacheResult: boolean;
- #loadCache?: MemoryLoadResultCache;
+ #loadCache?: LoadResultCache;
readonly watchFiles = new Set();
constructor(
private workspaceRoot: string,
private incremental: boolean,
options: BuildOptions | BundlerOptionsFactory,
- private alwaysUseContext = false,
+ private useContext = incremental,
private initialFilter?: (initial: Readonly) => boolean,
+ sharedLoadCache?: LoadResultCache,
) {
+ this.#loadCache = sharedLoadCache;
// To cache the results an option factory is needed to capture the full set of dependencies
this.#shouldCacheResult = incremental && typeof options === 'function';
this.#optionsFactory = (...args) => {
@@ -86,11 +104,11 @@ export class BundlerContext {
};
}
- static async bundleAll(
+ static bundleAll(
contexts: Iterable,
- changedFiles?: Iterable,
- ): Promise {
- const individualResults = await Promise.all(
+ changedFiles?: Iterable | ReadonlySet,
+ ): Promise {
+ return Promise.all(
[...contexts].map((context) => {
if (changedFiles) {
context.invalidate(changedFiles);
@@ -99,19 +117,13 @@ export class BundlerContext {
return context.bundle();
}),
);
-
- return BundlerContext.mergeResults(individualResults);
}
- static mergeResults(results: BundleContextResult[]): BundleContextResult {
- // Return directly if only one result
- if (results.length === 1) {
- return results[0];
- }
-
+ static mergeResults(results: BundleContextResult[]): BundleMergedContextResult {
let errors: Message[] | undefined;
const warnings: Message[] = [];
- const metafile: Metafile = { inputs: {}, outputs: {} };
+ const browserMetafile: Metafile = { inputs: {}, outputs: {} };
+ const serverMetafile: Metafile = { inputs: {}, outputs: {} };
const initialFiles = new Map();
const externalImportsBrowser = new Set();
const externalImportsServer = new Set();
@@ -126,17 +138,20 @@ export class BundlerContext {
continue;
}
- // Combine metafiles used for the stats option as well as bundle budgets and console output
+ const platformIsBrowser = result.platform === 'browser';
+
+ // Combine metafiles used for the bundle budgets and console output
if (result.metafile) {
+ const metafile = platformIsBrowser ? browserMetafile : serverMetafile;
Object.assign(metafile.inputs, result.metafile.inputs);
Object.assign(metafile.outputs, result.metafile.outputs);
}
- result.initialFiles.forEach((value, key) => initialFiles.set(key, value));
+ const externalImports = platformIsBrowser ? externalImportsBrowser : externalImportsServer;
+ result.externalImports?.forEach((value) => externalImports.add(value));
+ result.initialFiles.forEach((value, key) => initialFiles.set(key, value));
outputFiles.push(...result.outputFiles);
- result.externalImports.browser?.forEach((value) => externalImportsBrowser.add(value));
- result.externalImports.server?.forEach((value) => externalImportsServer.add(value));
if (result.externalConfiguration) {
externalConfiguration ??= new Set();
@@ -153,13 +168,16 @@ export class BundlerContext {
return {
errors,
warnings,
- metafile,
initialFiles,
outputFiles,
externalImports: {
browser: externalImportsBrowser,
server: externalImportsServer,
},
+ metafiles: {
+ browser: browserMetafile,
+ server: serverMetafile,
+ },
externalConfiguration: externalConfiguration ? [...externalConfiguration] : undefined,
};
}
@@ -203,7 +221,7 @@ export class BundlerContext {
async #performBundle(): Promise {
// Create esbuild options if not present
if (this.#esbuildOptions === undefined) {
- if (this.incremental) {
+ if (this.incremental && !this.#loadCache) {
this.#loadCache = new MemoryLoadResultCache();
}
this.#esbuildOptions = this.#optionsFactory(this.#loadCache);
@@ -218,7 +236,7 @@ export class BundlerContext {
if (this.#esbuildContext) {
// Rebuild using the existing incremental build context
result = await this.#esbuildContext.rebuild();
- } else if (this.incremental || this.alwaysUseContext) {
+ } else if (this.useContext) {
// Create a build context and perform the build.
// Context creation does not perform a build.
const esbuildContext = await context(this.#esbuildOptions);
@@ -242,23 +260,12 @@ export class BundlerContext {
// Build failures will throw an exception which contains errors/warnings
if (isEsBuildFailure(failure)) {
this.#addErrorsToWatch(failure);
+ this.#addLoadCacheFilesToWatch();
return failure;
} else {
throw failure;
}
- } finally {
- if (this.incremental) {
- // When incremental always add any files from the load result cache
- if (this.#loadCache) {
- for (const file of this.#loadCache.watchFiles) {
- if (!isInternalAngularFile(file)) {
- // watch files are fully resolved paths
- this.watchFiles.add(file);
- }
- }
- }
- }
}
// Update files that should be watched.
@@ -267,18 +274,38 @@ export class BundlerContext {
if (this.incremental) {
// Add input files except virtual angular files which do not exist on disk
for (const input of Object.keys(result.metafile.inputs)) {
- if (isInternalAngularFile(input) || isInternalBundlerFile(input)) {
- continue;
+ const isInternal = isInternalAngularFile(input) || isInternalBundlerFile(input);
+
+ // Input file paths are always relative to the workspace root unless already absolute
+ const normalizedAbsoluteInput = isAbsolute(input)
+ ? normalize(input)
+ : join(this.workspaceRoot, input);
+
+ if (!isInternal) {
+ this.watchFiles.add(normalizedAbsoluteInput);
}
- // Input file paths are always relative to the workspace root
- this.watchFiles.add(join(this.workspaceRoot, input));
+ if (this.#loadCache) {
+ const cachedLoad = await (this.#loadCache.get(input) ??
+ this.#loadCache.get(input.replace(';', ':')) ??
+ this.#loadCache.get('file:' + normalizedAbsoluteInput));
+ if (cachedLoad?.watchFiles) {
+ for (const file of cachedLoad.watchFiles) {
+ if (!isInternalAngularFile(file)) {
+ this.watchFiles.add(
+ isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file),
+ );
+ }
+ }
+ }
+ }
}
}
// Return if the build encountered any errors
if (result.errors.length) {
this.#addErrorsToWatch(result);
+ this.#addLoadCacheFilesToWatch();
return {
errors: result.errors,
@@ -416,9 +443,8 @@ export class BundlerContext {
...result,
outputFiles,
initialFiles,
- externalImports: {
- [isPlatformServer ? 'server' : 'browser']: externalImports,
- },
+ externalImports,
+ platform: isPlatformServer ? 'server' : 'browser',
externalConfiguration,
errors: undefined,
};
@@ -428,12 +454,22 @@ export class BundlerContext {
for (const error of result.errors) {
let file = error.location?.file;
if (file && !isInternalAngularFile(file)) {
- this.watchFiles.add(join(this.workspaceRoot, file));
+ this.watchFiles.add(isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file));
}
for (const note of error.notes) {
file = note.location?.file;
if (file && !isInternalAngularFile(file)) {
- this.watchFiles.add(join(this.workspaceRoot, file));
+ this.watchFiles.add(isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file));
+ }
+ }
+ }
+ }
+
+ #addLoadCacheFilesToWatch(): void {
+ if (this.incremental && this.#loadCache) {
+ for (const file of this.#loadCache.watchFiles) {
+ if (!isInternalAngularFile(file)) {
+ this.watchFiles.add(isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file));
}
}
}
@@ -446,19 +482,72 @@ export class BundlerContext {
* to be stored.
* @returns True, if the result was invalidated; False, otherwise.
*/
- invalidate(files: Iterable): boolean {
+ invalidate(files: Iterable | ReadonlySet): boolean {
if (!this.incremental) {
return false;
}
+ let candidateFiles: ReadonlySet;
+ if (files instanceof Set) {
+ let isCandidateReady = true;
+ for (const file of files) {
+ if (
+ file !== normalize(file) ||
+ (!isAbsolute(file) && !files.has(normalize(join(this.workspaceRoot, file))))
+ ) {
+ isCandidateReady = false;
+ break;
+ }
+ }
+
+ if (isCandidateReady) {
+ candidateFiles = files;
+ } else {
+ const normalizedFiles = new Set();
+ for (const file of files) {
+ const normalized = normalize(file);
+ normalizedFiles.add(normalized);
+ if (!isAbsolute(normalized)) {
+ normalizedFiles.add(normalize(join(this.workspaceRoot, normalized)));
+ }
+ }
+ candidateFiles = normalizedFiles;
+ }
+ } else {
+ const normalizedFiles = new Set();
+ for (const file of files) {
+ const normalized = normalize(file);
+ normalizedFiles.add(normalized);
+ if (!isAbsolute(normalized)) {
+ normalizedFiles.add(normalize(join(this.workspaceRoot, normalized)));
+ }
+ }
+ candidateFiles = normalizedFiles;
+ }
+
let invalid = false;
- for (const file of files) {
+ for (const file of candidateFiles) {
if (this.#loadCache?.invalidate(file)) {
invalid = true;
- continue;
}
+ }
- invalid ||= this.watchFiles.has(file);
+ if (!invalid) {
+ if (this.watchFiles.size < candidateFiles.size) {
+ for (const file of this.watchFiles) {
+ if (candidateFiles.has(file)) {
+ invalid = true;
+ break;
+ }
+ }
+ } else {
+ for (const file of candidateFiles) {
+ if (this.watchFiles.has(file)) {
+ invalid = true;
+ break;
+ }
+ }
+ }
}
if (invalid) {
diff --git a/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts b/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts
new file mode 100644
index 000000000000..213354389122
--- /dev/null
+++ b/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts
@@ -0,0 +1,113 @@
+/**
+ * @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 { BundlerContext } from './bundler-context';
+import { MemoryLoadResultCache } from './load-result-cache';
+
+describe('BundlerContext', () => {
+ describe('invalidate', () => {
+ it('should return false when incremental is disabled', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ false, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/app.css');
+
+ expect(context.invalidate(['/workspace/src/app.css'])).toBeFalse();
+ });
+
+ it('should return true when a watch file matches changed files as an array', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/app.css');
+
+ expect(context.invalidate(['/workspace/src/app.css'])).toBeTrue();
+ });
+
+ it('should return true when a watch file matches changed files as a ReadonlySet', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/app.css');
+
+ const changedSet: ReadonlySet = new Set(['/workspace/src/app.css']);
+ expect(context.invalidate(changedSet)).toBeTrue();
+ });
+
+ it('should return false when changed files do not intersect with watchFiles', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/app.css');
+
+ expect(context.invalidate(['/workspace/src/other.css'])).toBeFalse();
+ });
+
+ it('should correctly handle relative changed file paths', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/app.css');
+
+ expect(context.invalidate(['src/app.css'])).toBeTrue();
+ });
+
+ it('should correctly handle relative paths inside a ReadonlySet', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/app.css');
+
+ const set: ReadonlySet = new Set(['src/app.css']);
+ expect(context.invalidate(set)).toBeTrue();
+ });
+
+ it('should invalidate shared load cache when files change', async () => {
+ const loadCache = new MemoryLoadResultCache();
+ await loadCache.put('file:/workspace/src/app.css', {
+ contents: 'body {}',
+ loader: 'css',
+ watchFiles: ['/workspace/src/app.css'],
+ });
+
+ const context = new BundlerContext(
+ '/workspace',
+ /* incremental */ true,
+ () => ({ entryPoints: ['main.js'] }),
+ /* useContext */ false,
+ /* initialFilter */ undefined,
+ loadCache,
+ );
+
+ expect(loadCache.get('file:/workspace/src/app.css')).toBeDefined();
+ expect(context.invalidate(['/workspace/src/app.css'])).toBeTrue();
+ expect(loadCache.get('file:/workspace/src/app.css')).toBeUndefined();
+ });
+
+ it('should work when watchFiles is smaller than changed files', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ context.watchFiles.add('/workspace/src/file10.css');
+
+ const changedFiles = Array.from({ length: 100 }, (_, i) => `/workspace/src/file${i}.css`);
+ expect(context.invalidate(new Set(changedFiles))).toBeTrue();
+ });
+
+ it('should work when changed files is smaller than watchFiles', () => {
+ const context = new BundlerContext('/workspace', /* incremental */ true, () => ({
+ entryPoints: ['main.js'],
+ }));
+ for (let i = 0; i < 100; i++) {
+ context.watchFiles.add(`/workspace/src/file${i}.css`);
+ }
+
+ expect(context.invalidate(new Set(['/workspace/src/file50.css']))).toBeTrue();
+ });
+ });
+});
diff --git a/packages/angular/build/src/tools/esbuild/bundler-files.ts b/packages/angular/build/src/tools/esbuild/bundler-files.ts
index ac33d471a395..168c41e65940 100644
--- a/packages/angular/build/src/tools/esbuild/bundler-files.ts
+++ b/packages/angular/build/src/tools/esbuild/bundler-files.ts
@@ -7,7 +7,7 @@
*/
import type { OutputFile } from 'esbuild';
-import { createHash } from 'node:crypto';
+import { calculateHash } from '../../utils/hash';
export interface InitialFileRecord {
entrypoint: boolean;
@@ -63,9 +63,7 @@ export function createOutputFile(
return this.contents.byteLength;
},
get hash(): string {
- cachedHash ??= createHash('sha256')
- .update(cachedText ?? this.contents)
- .digest('hex');
+ cachedHash ??= calculateHash(cachedText ?? this.contents);
return cachedHash;
},
@@ -97,7 +95,7 @@ export function createOutputFile(
return cachedText;
},
get hash(): string {
- cachedHash ??= createHash('sha256').update(this.contents).digest('hex');
+ cachedHash ??= calculateHash(this.contents);
return cachedHash;
},
diff --git a/packages/angular/build/src/tools/esbuild/cache.ts b/packages/angular/build/src/tools/esbuild/cache.ts
index 2bb6afcb725f..5f1cd0d2927b 100644
--- a/packages/angular/build/src/tools/esbuild/cache.ts
+++ b/packages/angular/build/src/tools/esbuild/cache.ts
@@ -195,6 +195,15 @@ export class Cache = CacheStore> {
await this.store.set(namespacedKey, value);
}
+ /**
+ * Clears internal state for a specific namespaced key (requests, write counts, and pending gets).
+ */
+ protected deleteInternal(namespacedKey: string): void {
+ this.#requests.delete(namespacedKey);
+ this.#writeCounts.delete(namespacedKey);
+ this.#pendingGets.delete(namespacedKey);
+ }
+
/**
* Clears the base class internal state (requests, write counts, and pending gets).
*/
@@ -213,6 +222,18 @@ export class MemoryCache extends Cache> {
super(new Map());
}
+ /**
+ * Removes the specified key from the cache instance.
+ * @param key The key to remove.
+ * @returns True if an element in the Map existed and has been removed, or false if the element does not exist.
+ */
+ delete(key: string): boolean {
+ const namespacedKey = this.withNamespace(key);
+ this.deleteInternal(namespacedKey);
+
+ return this.store.delete(namespacedKey);
+ }
+
/**
* Removes all entries from the cache instance.
*/
diff --git a/packages/angular/build/src/tools/esbuild/cache_spec.ts b/packages/angular/build/src/tools/esbuild/cache_spec.ts
index a30852836eff..a81bfbd25acd 100644
--- a/packages/angular/build/src/tools/esbuild/cache_spec.ts
+++ b/packages/angular/build/src/tools/esbuild/cache_spec.ts
@@ -141,4 +141,21 @@ describe('MemoryCache', () => {
const val2 = await cache.getOrCreate('key', () => 'should-not-run');
expect(val2).toBe('override-value');
});
+
+ it('should delete a cached key and allow creating a new value', async () => {
+ await cache.put('key', 'value-1');
+ expect(await cache.get('key')).toBe('value-1');
+
+ const deleted = cache.delete('key');
+ expect(deleted).toBeTrue();
+ expect(await cache.get('key')).toBeUndefined();
+
+ // Subsequent getOrCreate should call creator
+ const newValue = await cache.getOrCreate('key', () => 'value-2');
+ expect(newValue).toBe('value-2');
+ });
+
+ it('should return false when deleting a non-existent key', () => {
+ expect(cache.delete('non-existent')).toBeFalse();
+ });
});
diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
index bfec753f6b2d..af9d9dce134b 100644
--- a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
+++ b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
@@ -7,11 +7,14 @@
*/
import remapping, { type DecodedSourceMap, type SourceMapInput } from '@ampproject/remapping';
+import type { Node } from '@oxc-project/types';
import { MagicString } from 'magic-string';
import assert from 'node:assert';
import { deserialize } from 'node:v8';
import { workerData } from 'node:worker_threads';
-import { Visitor, parseSync } from 'oxc-parser';
+import { parseSync, visitorKeys } from 'oxc-parser';
+import { loadLocaleData } from './i18n-locale-plugin';
+import { createSharedTranslationProxy } from './i18n-translation-reader';
/**
* The options passed to the inliner for each file request
@@ -30,10 +33,10 @@ interface InlineFileRequest {
/**
* The serialized translation messages for the locale that should be used during the inlining
- * process of the file. A Blob is used so that the messages are shared with the Worker by
- * reference instead of being copied into it for every request.
+ * process of the file. A SharedArrayBuffer or Blob is used so that the messages are shared with
+ * the Worker by reference instead of being copied into it for every request.
*/
- translation?: Blob;
+ translation?: Blob | SharedArrayBuffer;
}
/**
@@ -57,52 +60,152 @@ interface InlineCodeRequest {
/**
* The serialized translation messages for the locale that should be used during the inlining
- * process of the file. A Blob is used so that the messages are shared with the Worker by
- * reference instead of being copied into it for every request.
+ * process of the file. A SharedArrayBuffer or Blob is used so that the messages are shared with
+ * the Worker by reference instead of being copied into it for every request.
*/
- translation?: Blob;
+ translation?: Blob | SharedArrayBuffer;
+}
+
+/**
+ * The options passed to the inliner for a batch file request
+ */
+interface InlineFileBatchRequest {
+ /**
+ * The filename that should be processed. The data for the file is provided to the Worker
+ * during Worker initialization.
+ */
+ filename: string;
+
+ /**
+ * The locale specifiers or locale objects that should be used during the inlining process of the file.
+ */
+ locales: (string | { locale: string; translation?: Blob | SharedArrayBuffer })[];
+
+ /**
+ * Whether the file data should be treated as ephemeral and not cached long-term in the Worker.
+ * Typically true when all remaining locales for the file are processed in a single batch.
+ */
+ ephemeral?: boolean;
+
+ /**
+ * The list of active locales in the current inlining window. Any cached translation dictionaries
+ * not present in this list will be evicted from the Worker's memory cache.
+ */
+ activeLocales?: string[];
+}
+
+/**
+ * The result for a single locale within a batch file request.
+ */
+interface InlineLocaleResult {
+ locale: string;
+ code: string;
+ map?: string;
+ messages: { type: 'error' | 'warning'; message: string }[];
+}
+
+/**
+ * The response returned from a batch file request.
+ */
+interface InlineFileBatchResult {
+ file: string;
+ results: InlineLocaleResult[];
}
// Extract the application files and common options used for inline requests from the Worker context
-const { files, missingTranslation, shouldOptimize } = (workerData || {}) as {
+const { files, missingTranslation, translations } = (workerData || {}) as {
files: ReadonlyMap;
missingTranslation: 'error' | 'warning' | 'ignore';
- shouldOptimize: boolean;
+ translations?: ReadonlyMap;
};
/**
- * The translation messages deserialized for the locale most recently requested of this Worker.
- * Locales are inlined one at a time, so retaining only the active locale is enough to avoid
- * deserializing the messages once per file while holding at most one set of messages in memory.
+ * Cached file data including code and extracted localization metadata.
*/
-let activeTranslation: { locale: string; messages: Promise> } | undefined;
+interface CachedFileData {
+ code: string;
+ metadata: FileLocalizeMetadata;
+}
+
+/**
+ * Cache of file data promises keyed by filename.
+ */
+const fileDataCache = new Map>();
+
+/**
+ * Cache of deserialized translation messages keyed by locale.
+ */
+const deserializedTranslations = new Map>>();
+
+/**
+ * Retrieves the file data for a filename, loading and extracting localization metadata.
+ * If `cache` is true, the result is cached in `fileDataCache` across requests in this Worker.
+ * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`, allowing it
+ * to be garbage-collected once the batch request finishes.
+ *
+ * @param filename The name of the file to load.
+ * @param cache Whether to cache the loaded file data in the Worker's long-term cache.
+ * @returns The cached or newly extracted code and localization metadata.
+ */
+function loadFileData(filename: string, cache = true): Promise {
+ const existing = fileDataCache.get(filename);
+ if (existing) {
+ return existing;
+ }
+
+ const fileDataPromise = (async () => {
+ const data = files.get(filename);
+ assert(data !== undefined, `Invalid inline request for file '${filename}'.`);
+
+ const code = await data.text();
+ const metadata = extractLocalizeMetadata(filename, code);
+
+ return { code, metadata };
+ })();
+
+ if (cache) {
+ fileDataPromise.catch(() => {
+ fileDataCache.delete(filename);
+ });
+ fileDataCache.set(filename, fileDataPromise);
+ }
+
+ return fileDataPromise;
+}
/**
- * Deserializes the translation messages for an inline request, reusing the result for any
+ * Deserializes or wraps the translation messages for a locale, reusing the result for any
* subsequent request that targets the same locale.
- * @param request An inline request containing the locale and its serialized messages.
+ * @param locale The locale identifier.
+ * @param translation Optional serialized translation messages (SharedArrayBuffer or Blob).
* @returns The translation messages, or undefined if the locale has no translations.
*/
function loadTranslation(
- request: InlineFileRequest | InlineCodeRequest,
+ locale: string,
+ translation?: Blob | SharedArrayBuffer,
): Promise> | undefined {
- const { locale, translation } = request;
- if (!translation) {
+ const translationData = translation ?? translations?.get(locale);
+ if (!translationData) {
return undefined;
}
- if (activeTranslation?.locale !== locale) {
- activeTranslation = {
- locale,
- // Deserializing within the stored promise ensures that concurrent requests for a locale
- // share the one deserialization instead of each performing their own.
- messages: translation
+ let messagesPromise = deserializedTranslations.get(locale);
+ if (!messagesPromise) {
+ if (translationData instanceof Blob) {
+ messagesPromise = translationData
.arrayBuffer()
- .then((buffer) => deserialize(new Uint8Array(buffer)) as Record),
- };
+ .then((buffer) => deserialize(new Uint8Array(buffer)) as Record)
+ .catch((error) => {
+ deserializedTranslations.delete(locale);
+ throw error;
+ });
+ } else {
+ messagesPromise = Promise.resolve(createSharedTranslationProxy(translationData));
+ }
+ deserializedTranslations.set(locale, messagesPromise);
}
- return activeTranslation.messages;
+ return messagesPromise;
}
/**
@@ -113,17 +216,20 @@ function loadTranslation(
* @returns An object containing the inlined file and optional map content.
*/
export default async function inlineFile(request: InlineFileRequest) {
- const data = files.get(request.filename);
+ const { code, metadata } = await loadFileData(request.filename, true);
- assert(data !== undefined, `Invalid inline request for file '${request.filename}'.`);
+ // Sourcemaps are parsed on demand per request rather than cached long-term to prevent
+ // monotonic memory growth as a worker processes multiple files across the build.
+ const rawMap = await files.get(request.filename + '.map')?.text();
+ const map = rawMap ? (JSON.parse(rawMap) as SourceMapInput) : undefined;
- const code = await data.text();
- const map = await files.get(request.filename + '.map')?.text();
- const result = await transformWithOxc(
+ const result = await inlineLocalize(
code,
- map && (JSON.parse(map) as SourceMapInput),
- request,
- await loadTranslation(request),
+ map,
+ metadata,
+ request.locale,
+ await loadTranslation(request.locale, request.translation),
+ request.filename,
);
return {
@@ -134,6 +240,59 @@ export default async function inlineFile(request: InlineFileRequest) {
};
}
+/**
+ * Inlines multiple locales and translations into a JavaScript file that contains `$localize` usage.
+ *
+ * @param request An InlineFileBatchRequest object representing the options for inlining.
+ * @returns An object containing the inlined results for each requested locale.
+ */
+export async function inlineFileBatch(
+ request: InlineFileBatchRequest,
+): Promise {
+ if (request.activeLocales) {
+ const activeSet = new Set(request.activeLocales);
+ for (const locale of deserializedTranslations.keys()) {
+ if (!activeSet.has(locale)) {
+ deserializedTranslations.delete(locale);
+ }
+ }
+ }
+
+ const { code, metadata } = await loadFileData(request.filename, !request.ephemeral);
+
+ // Parse the sourcemap once for the entire batch.
+ // It will naturally be garbage-collected after this batch action returns.
+ const rawMap = await files.get(request.filename + '.map')?.text();
+ const map = rawMap ? (JSON.parse(rawMap) as SourceMapInput) : undefined;
+
+ const results = await Promise.all(
+ request.locales.map(async (entry) => {
+ const locale = typeof entry === 'string' ? entry : entry.locale;
+ const translation = typeof entry === 'string' ? undefined : entry.translation;
+ const result = await inlineLocalize(
+ code,
+ map,
+ metadata,
+ locale,
+ await loadTranslation(locale, translation),
+ request.filename,
+ );
+
+ return {
+ locale,
+ code: result.code,
+ map: result.map,
+ messages: result.diagnostics.messages,
+ };
+ }),
+ );
+
+ return {
+ file: request.filename,
+ results,
+ };
+}
+
/**
* Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
* This function is a secondary entry primarily for use with component HMR update modules.
@@ -142,11 +301,14 @@ export default async function inlineFile(request: InlineFileRequest) {
* @returns An object containing the inlined code.
*/
export async function inlineCode(request: InlineCodeRequest) {
- const result = await transformWithOxc(
+ const metadata = extractLocalizeMetadata(request.filename, request.code);
+ const result = await inlineLocalize(
request.code,
undefined,
- request,
- await loadTranslation(request),
+ metadata,
+ request.locale,
+ await loadTranslation(request.locale, request.translation),
+ request.filename,
);
return {
@@ -181,81 +343,219 @@ async function loadLocalizeTools(): Promise {
}
/**
- * Transforms a JavaScript file using OXC and Magic-String to inline the request locale and translation.
- * @param code A string containing the JavaScript code to transform.
- * @param map A sourcemap object for the provided JavaScript code.
- * @param options The inline request options to use.
- * @param translation The translation messages to inline, or undefined for an untranslated locale.
- * @returns An object containing the code, map, and diagnostics from the transformation.
+ * Traverses ESTree AST nodes in post-order (bottom-up) without recursion.
+ * Bottom-up traversal ensures that nested `$localize` expressions are transformed and
+ * written to MagicString before outer containing templates are evaluated.
+ *
+ * @param root The root AST node to traverse.
+ * @param onExit Callback invoked on each AST node in post-order.
*/
-async function transformWithOxc(
- code: string,
- map: SourceMapInput | undefined,
- options: InlineFileRequest,
- translation: Record | undefined,
-) {
- const { program } = parseSync(options.filename, code, {
+function walkAstPostOrder(root: Node, onExit: (node: Node) => void): void {
+ const traverseStack: Node[] = [root];
+ const postOrderNodes: Node[] = [];
+
+ while (traverseStack.length > 0) {
+ const current = traverseStack.pop();
+ if (!current) {
+ continue;
+ }
+
+ postOrderNodes.push(current);
+
+ const keys = visitorKeys[current.type];
+ if (!keys) {
+ continue;
+ }
+
+ for (let i = 0; i < keys.length; i++) {
+ const child = (current as unknown as Record)[keys[i]];
+ if (!child) {
+ continue;
+ }
+
+ if (Array.isArray(child)) {
+ for (const item of child) {
+ if (item) {
+ traverseStack.push(item);
+ }
+ }
+ } else {
+ traverseStack.push(child);
+ }
+ }
+ }
+
+ // Process collected nodes in reverse order to achieve bottom-up (post-order) traversal
+ for (let i = postOrderNodes.length - 1; i >= 0; i--) {
+ onExit(postOrderNodes[i]);
+ }
+}
+
+/**
+ * Metadata for a `$localize` tagged template expression extracted from the AST.
+ */
+interface LocalizeCallSite {
+ start: number;
+ end: number;
+ messageParts: TemplateStringsArray;
+ expressions: { start: number; end: number }[];
+}
+
+/**
+ * Metadata extracted from a JavaScript file AST needed for localization inlining.
+ */
+interface FileLocalizeMetadata {
+ callSites: LocalizeCallSite[];
+ localeInsertSites: { start: number; end: number }[];
+ diagnostics?: string[];
+}
+
+/**
+ * Extracts localization call sites and locale insertion points from JavaScript code using OXC.
+ *
+ * @param filename The name of the file being processed.
+ * @param code The JavaScript source code.
+ * @returns The extracted localization metadata.
+ */
+function extractLocalizeMetadata(filename: string, code: string): FileLocalizeMetadata {
+ const { program } = parseSync(filename, code, {
sourceType: 'unambiguous',
});
if (!program) {
- throw new Error(`Unknown error occurred parsing file "${options.filename}" with OXC.`);
+ throw new Error(`Unknown error occurred parsing file "${filename}" with OXC.`);
}
- const magicString = new MagicString(code);
- const { Diagnostics, translate } = await loadLocalizeTools();
- const diagnostics = new Diagnostics();
+ const callSites: LocalizeCallSite[] = [];
+ const localeInsertSites: { start: number; end: number }[] = [];
+ let diagnostics: string[] | undefined;
- const visitor = new Visitor({
- Literal(node) {
+ walkAstPostOrder(program, (node) => {
+ if (node.type === 'Literal') {
if (typeof node.value === 'string' && node.value === '___NG_LOCALE_INSERT___') {
- magicString.overwrite(node.start, node.end, JSON.stringify(options.locale));
+ localeInsertSites.push({ start: node.start, end: node.end });
}
- },
- 'TaggedTemplateExpression:exit'(node) {
+ } else if (node.type === 'TaggedTemplateExpression') {
if (node.tag.type === 'Identifier' && node.tag.name === '$localize') {
- const cooked = node.quasi.quasis.map((q) => q.value.cooked);
- const raw = node.quasi.quasis.map((q) => q.value.raw);
- const messageParts = Object.assign(cooked, { raw }) as unknown as TemplateStringsArray;
-
- const [translatedParts, translatedSubstitutions] = translate(
- diagnostics,
- translation || {},
- messageParts,
- node.quasi.expressions.map((_, index) => index),
- translation === undefined ? 'ignore' : missingTranslation,
- );
-
- // Reconstruct the new template/string literal replacement
- let replacement: string;
- if (translatedSubstitutions.length === 0) {
- replacement = JSON.stringify(translatedParts[0]);
- } else {
- replacement = '`';
- for (let i = 0; i < translatedParts.length; i++) {
- const escapedPart = JSON.stringify(translatedParts[i])
- .slice(1, -1)
- .replace(/\\"/g, '"')
- .replace(/`/g, '\\`')
- .replace(/\$\{/g, '\\${');
- replacement += escapedPart;
-
- if (i < translatedSubstitutions.length) {
- const originalIndex = translatedSubstitutions[i];
- const exprNode = node.quasi.expressions[originalIndex];
- const exprCode = magicString.slice(exprNode.start, exprNode.end);
- replacement += '${' + exprCode + '}';
- }
+ const cooked: string[] = [];
+ const raw: string[] = [];
+ let hasMalformedEscape = false;
+
+ for (const q of node.quasi.quasis) {
+ if (q.value.cooked === null || q.value.cooked === undefined) {
+ hasMalformedEscape = true;
+ (diagnostics ??= []).push(
+ `Malformed escape sequence in $localize template literal in file "${filename}".`,
+ );
+ break;
}
- replacement += '`';
+ cooked.push(q.value.cooked);
+ raw.push(q.value.raw);
}
- magicString.overwrite(node.start, node.end, replacement);
+ if (!hasMalformedEscape) {
+ const messageParts = Object.assign(cooked, { raw });
+ const expressions = node.quasi.expressions.map((expr) => ({
+ start: expr.start,
+ end: expr.end,
+ }));
+
+ callSites.push({
+ start: node.start,
+ end: node.end,
+ messageParts,
+ expressions,
+ });
+ }
}
- },
+ }
});
- visitor.visit(program);
+ return { callSites, localeInsertSites, diagnostics };
+}
+
+/**
+ * Inlines translations into code using previously extracted localization metadata.
+ *
+ * @param code The source code to transform.
+ * @param map Optional source map for the source code.
+ * @param metadata Extracted localization metadata.
+ * @param locale The target locale identifier.
+ * @param translation The translation messages dictionary, or undefined for untranslated locale.
+ * @param filename The name of the file being transformed.
+ * @returns The transformed code, optional remapped source map, and diagnostics.
+ */
+async function inlineLocalize(
+ code: string,
+ map: SourceMapInput | undefined,
+ metadata: FileLocalizeMetadata,
+ locale: string,
+ translation: Record | undefined,
+ filename: string,
+) {
+ const magicString = new MagicString(code);
+ const { Diagnostics, translate } = await loadLocalizeTools();
+ const diagnostics = new Diagnostics();
+
+ if (metadata.diagnostics) {
+ for (const message of metadata.diagnostics) {
+ diagnostics.error(message);
+ }
+ }
+
+ if (metadata.localeInsertSites.length > 0) {
+ const localeData = await loadLocaleData(locale);
+ if (localeData.error) {
+ diagnostics.error(localeData.error);
+ } else if (localeData.warning) {
+ diagnostics.warn(localeData.warning);
+ }
+ let injected = false;
+ for (const site of metadata.localeInsertSites) {
+ magicString.overwrite(
+ site.start,
+ site.end,
+ JSON.stringify(locale) + (localeData.code && !injected ? `;\n${localeData.code}\n;` : ''),
+ );
+ injected = true;
+ }
+ }
+
+ for (const callSite of metadata.callSites) {
+ const [translatedParts, translatedSubstitutions] = translate(
+ diagnostics,
+ translation || {},
+ callSite.messageParts,
+ callSite.expressions.map((_, index) => index),
+ translation === undefined ? 'ignore' : missingTranslation,
+ );
+
+ // Reconstruct the new template/string literal replacement
+ let replacement: string;
+ if (translatedSubstitutions.length === 0) {
+ replacement = JSON.stringify(translatedParts[0]);
+ } else {
+ replacement = '`';
+ for (let i = 0; i < translatedParts.length; i++) {
+ const escapedPart = JSON.stringify(translatedParts[i])
+ .slice(1, -1)
+ .replace(/\\"/g, '"')
+ .replace(/`/g, '\\`')
+ .replace(/\$\{/g, '\\${');
+ replacement += escapedPart;
+
+ if (i < translatedSubstitutions.length) {
+ const originalIndex = translatedSubstitutions[i];
+ const expr = callSite.expressions[originalIndex];
+ const exprCode = magicString.slice(expr.start, expr.end);
+ replacement += '${' + exprCode + '}';
+ }
+ }
+ replacement += '`';
+ }
+
+ magicString.overwrite(callSite.start, callSite.end, replacement);
+ }
const outputCode = magicString.toString();
let outputMap;
@@ -264,7 +564,7 @@ async function transformWithOxc(
// inputs. Encoding the mappings only for remapping to immediately decode them again doubles
// the peak memory of the largest structure involved in inlining a file.
const rawMap = magicString.generateDecodedMap({
- source: options.filename,
+ source: filename,
includeContent: true,
hires: 'boundary',
});
diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
index 071097315e5b..41de55de666f 100644
--- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
+++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
@@ -7,12 +7,13 @@
*/
import assert from 'node:assert';
-import { createHash } from 'node:crypto';
import { extname, join } from 'node:path';
import { serialize } from 'node:v8';
+import { calculateHash, createContentHash, initializeHash } from '../../utils/hash';
import { WorkerPool } from '../../utils/worker-pool';
import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files';
-import { type PersistentCacheStore, createPersistentCacheStore } from './cache';
+import { type Cache, type PersistentCacheStore, createPersistentCacheStore } from './cache';
+import { encodeTranslationToBuffer } from './i18n-translation-encoder';
/**
* A keyword used to indicate if a JavaScript file may require inlining of translations.
@@ -20,18 +21,34 @@ import { type PersistentCacheStore, createPersistentCacheStore } from './cache';
*/
const LOCALIZE_KEYWORD = '$localize';
+/**
+ * The maximum number of locales to process concurrently in a single sliding window.
+ * This caps peak worker memory while maintaining multi-locale batching throughput.
+ */
+const DEFAULT_LOCALE_WINDOW_SIZE = 8;
+
/**
* Serializes the translation messages for a locale for transfer to an inliner Worker.
*
- * A Blob is used because cloning one shares its data by reference, whereas sending the messages
- * themselves copies them into a Worker for every request that carries them. A locale can contain
- * tens of thousands of messages, which makes that copy the dominant cost of inlining a locale.
+ * A SharedArrayBuffer is preferred because it enables zero-copy shared memory access
+ * and on-demand string decoding across all worker threads. Falls back to a Blob when
+ * SharedArrayBuffer is unavailable.
*
* @param translation The translation messages for a locale, if the locale has any.
- * @returns A Blob containing the serialized messages, or undefined if the locale has none.
+ * @returns A SharedArrayBuffer or Blob containing the serialized messages, or undefined if none.
*/
-function serializeTranslation(translation: Record | undefined): Blob | undefined {
- return translation && new Blob([serialize(translation)]);
+function serializeTranslation(
+ translation: Record | undefined,
+): SharedArrayBuffer | Blob | undefined {
+ if (!translation) {
+ return undefined;
+ }
+
+ if (typeof SharedArrayBuffer !== 'undefined') {
+ return encodeTranslationToBuffer(translation);
+ }
+
+ return new Blob([serialize(translation)]);
}
/**
@@ -42,6 +59,81 @@ export interface I18nInlinerOptions {
outputFiles: BuildOutputFile[];
shouldOptimize?: boolean;
persistentCachePath?: string;
+ localizeVersion?: string;
+ translations?: ReadonlyMap;
+}
+
+/**
+ * Options for inlining a specific locale.
+ */
+export interface LocaleInlineOptions {
+ /**
+ * The locale specifier string.
+ */
+ locale: string;
+
+ /**
+ * The translation messages for the locale, or undefined for the source/untranslated locale.
+ */
+ translation?: Record;
+
+ /**
+ * An optional content integrity hash of the translation file(s) for fast cache key calculation.
+ */
+ translationIntegrity?: string;
+}
+
+/**
+ * Result of inlining for a specific locale.
+ */
+export interface LocaleInlineResult {
+ outputFiles: BuildOutputFile[];
+ errors: string[];
+ warnings: string[];
+}
+
+/**
+ * Transformation result for a single file and locale combination.
+ */
+interface TransformedFileResult {
+ file: string;
+ code: string;
+ map?: string;
+ messages: { type: 'error' | 'warning'; message: string }[];
+}
+
+/**
+ * Represents an in-flight asynchronous cache lookup for a single (file x locale) transformation.
+ */
+interface CacheCheckItem {
+ /**
+ * The relative file path of the JavaScript file to transform.
+ */
+ filename: string;
+
+ /**
+ * The locale specifier being targeted for translation.
+ */
+ locale: string;
+
+ /**
+ * The computed cache key hash, or undefined if persistent caching is not configured.
+ */
+ cacheKey: string | undefined;
+
+ /**
+ * A promise that resolves to the cached transform result, or null if uncached or on lookup failure.
+ */
+ cachedResult: Promise;
+}
+
+/**
+ * An uncached transformation request entry for a file within a specific locale.
+ */
+interface UncachedLocaleEntry {
+ locale: string;
+ cacheKey?: string;
+ translation?: Blob | SharedArrayBuffer;
}
/**
@@ -53,7 +145,8 @@ export interface I18nInlinerOptions {
export class I18nInliner {
#cacheInitFailed = false;
#workerPool: WorkerPool;
- #cache: PersistentCacheStore | undefined;
+ #cacheStore: PersistentCacheStore | undefined;
+ #cache: Cache | undefined;
readonly #localizeFiles: ReadonlyMap;
readonly #unmodifiedFiles: Array;
@@ -62,7 +155,7 @@ export class I18nInliner {
maxThreads?: number,
) {
this.#unmodifiedFiles = [];
- const { outputFiles, shouldOptimize, missingTranslation } = options;
+ const { outputFiles, shouldOptimize, missingTranslation, translations } = options;
const files = new Map();
const pendingMaps = [];
@@ -114,6 +207,7 @@ export class I18nInliner {
workerData: {
missingTranslation,
shouldOptimize,
+ translations,
// A Blob is an immutable data structure that allows sharing the data between workers
// without copying until the data is actually used within a Worker. This is useful here
// since each file may not actually be processed in each Worker and the Blob avoids
@@ -126,113 +220,272 @@ export class I18nInliner {
}
/**
- * Performs inlining of translations for the provided locale and translations. The files that
- * are processed originate from the files passed to the class constructor and filter by presence
- * of the localize function keyword.
- * @param locale The string representing the locale to inline.
- * @param translation The translation messages to use when inlining.
- * @returns A promise that resolves to an array of OutputFiles representing a translated result.
+ * Performs inlining of translations across multiple locales in parallel.
+ *
+ * An adaptive 2D task-partitioning algorithm distributes (files x locales) work units
+ * across all worker threads while caching AST metadata and sourcemaps in worker memory.
+ *
+ * @param locales The locales and translations to inline.
+ * @returns A map of locale names to their inlined output files and diagnostics.
*/
- async inlineForLocale(
- locale: string,
- translation: Record | undefined,
- ): Promise<{ outputFiles: BuildOutputFile[]; errors: string[]; warnings: string[] }> {
+ async inlineAll(
+ locales: Iterable,
+ ): Promise