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 94fdfb8df840..7bcd8eaa270d 100644
--- a/.github/workflows/assistant-to-the-branch-manager.yml
+++ b/.github/workflows/assistant-to-the-branch-manager.yml
@@ -2,6 +2,7 @@ name: DevInfra
on:
push:
+ # zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, labeled]
@@ -17,6 +18,6 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- - uses: angular/dev-infra/github-actions/branch-manager@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ - uses: angular/dev-infra/github-actions/branch-manager@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 57b31cb9c2f1..8395ac196c14 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Run CLI E2E tests
@@ -208,34 +208,35 @@ 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
+ - name: Start Sauce Connect
+ uses: saucelabs/sauce-connect-action@cb88b508c6f9ff4d84490093733315dbd55de022 # v3
+ timeout-minutes: 10
+ with:
+ username: ${{ vars.SAUCE_USERNAME }}
+ accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
+ region: us-west-1
+ tunnelName: ${{ env.SAUCE_TUNNEL_IDENTIFIER }}
+ proxyLocalhost: allow
+ # By default we disable SSL bumping for all requests. This is because SSL bumping is not needed
+ # for our test setup and in order to perform the SSL bumping, Saucelabs intercepts all HTTP
+ # requests in the tunnel VM and modifies them. This can cause flakiness as it makes all requests
+ # dependent on the SSL bumping middleware.
+ # See: https://wiki.saucelabs.com/display/DOCS/Troubleshooting+Sauce+Connect#TroubleshootingSauceConnect-DisablingSSLBumping
+ tlsPassthroughDomains: all
- name: Run E2E Browser tests
env:
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
- SAUCE_LOG_FILE: /tmp/angular/sauce-connect.log
- SAUCE_READY_FILE: /tmp/angular/sauce-connect-ready-file.lock
- SAUCE_PID_FILE: /tmp/angular/sauce-connect-pid-file.lock
- SAUCE_TUNNEL_IDENTIFIER: 'angular-${{ github.run_number }}'
- SAUCE_READY_FILE_TIMEOUT: 120
- run: |
- ./scripts/saucelabs/start-tunnel.sh &
- ./scripts/saucelabs/wait-for-tunnel.sh
- pnpm bazel test --config=saucelabs //tests:e2e.saucelabs
- ./scripts/saucelabs/stop-tunnel.sh
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- if: ${{ failure() }}
- with:
- name: sauce-connect-log
- path: ${{ env.SAUCE_CONNECT_DIR_IN_HOST }}/sauce-connect.log
+ run: pnpm bazel test --config=saucelabs //tests:e2e.saucelabs
publish-snapshots:
needs: build
@@ -244,11 +245,11 @@ jobs:
CIRCLE_BRANCH: ${{ github.ref_name }}
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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 f95ab2f4ac05..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@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
- with:
- languages: javascript-typescript
- build-mode: none
- config-file: .github/codeql/config.yml
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
- with:
- category: '/language:javascript-typescript'
diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml
index e0efdf2b94e4..3ff292ec3ff9 100644
--- a/.github/workflows/dev-infra.yml
+++ b/.github/workflows/dev-infra.yml
@@ -1,6 +1,7 @@
name: DevInfra
on:
+ # zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
pull_request_target:
types: [opened, synchronize, reopened]
issues:
@@ -15,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@649c3afeaa46674507b9625537e49de54a695e2b
+ - uses: angular/dev-infra/github-actions/labeling/pull-request@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@649c3afeaa46674507b9625537e49de54a695e2b
+ - uses: angular/dev-infra/github-actions/post-approval-changes@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@649c3afeaa46674507b9625537e49de54a695e2b
+ - uses: angular/dev-infra/github-actions/labeling/issue@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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 dac67ebcbe7f..8b7edf35f34b 100644
--- a/.github/workflows/perf.yml
+++ b/.github/workflows/perf.yml
@@ -9,7 +9,6 @@ on:
permissions:
contents: 'read'
- id-token: 'write'
defaults:
run:
@@ -23,7 +22,7 @@ jobs:
workflows: ${{ steps.workflows.outputs.workflows }}
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- id: workflows
@@ -33,14 +32,17 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: list
+ permissions:
+ contents: 'read'
+ id-token: 'write'
strategy:
matrix:
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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
@@ -52,4 +54,7 @@ jobs:
project_id: 'internal-200822'
workload_identity_provider: 'projects/823469418460/locations/global/workloadIdentityPools/measurables-tracking/providers/angular'
service_account: 'measures-uploader@internal-200822.iam.gserviceaccount.com'
- - run: pnpm ng-dev perf workflows --name ${{ matrix.workflow }} --commit-sha ${{github.sha}}
+ - run: pnpm ng-dev perf workflows --name "$WORKFLOW" --commit-sha "$COMMIT_SHA"
+ env:
+ WORKFLOW: ${{ matrix.workflow }}
+ COMMIT_SHA: ${{ github.sha }}
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 0f5a37f5751a..44071b0478ef 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/linting/licenses@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
build:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
- uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # 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 2e9f8b03919a..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@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
- 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@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
- with:
- sarif_file: results.sarif
diff --git a/.nvmrc b/.nvmrc
index f9e7451e7715..60ade1ae01e8 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.23.1
+24.19.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8867117ff498..f3077ffb76bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,172 +1,519 @@
-
+
-# 22.0.8 (2026-07-23)
+# 22.2.0-next.4 (2026-08-19)
### @angular/cli
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- |
-| [41c356256](https://github.com/angular/angular-cli/commit/41c356256525469a41ea72896b1ddff91a4e6e55) | fix | batch Prettier invocations during migration formatting |
-| [bc87c85c3](https://github.com/angular/angular-cli/commit/bc87c85c3c660e969cf77a493dc4ee58fd831214) | fix | update MCP devserver tool names to conform to spec regex |
+| 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 |
-| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- |
-| [5724bf632](https://github.com/angular/angular-cli/commit/5724bf632eba59eccdd941beae8aafad35ce003e) | fix | canonicalize drive letter casing for workspace root on Windows |
-| [b8bdbaa1e](https://github.com/angular/angular-cli/commit/b8bdbaa1ea1c58f061580bd58c015078f57c1530) | fix | ensure import map integrity keys are valid URL-like specifiers |
-| [212a7bb21](https://github.com/angular/angular-cli/commit/212a7bb21eca131f32fd7b4e7b5a9ab87f2bfded) | fix | favor istanbul coverage provider when browser testing is enabled |
-| [bc760b005](https://github.com/angular/angular-cli/commit/bc760b005e0dae10a2dfd31a7f0f772482305398) | fix | preserve custom config options in runnerConfig for vitest |
-| [19cda7415](https://github.com/angular/angular-cli/commit/19cda7415693f36e911a736a32576110905b6600) | fix | remap metafile paths when workspace root is a symlink or junction |
-| [fdcc98c79](https://github.com/angular/angular-cli/commit/fdcc98c7912f45431268fdf66c8b132bd119bc27) | perf | skip semantic affected-file walk when type checking is disabled |
+| 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.0-next.4 (2026-07-15)
+# 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 |
| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ |
-| [3789ceee1](https://github.com/angular/angular-cli/commit/3789ceee18b4528c2acb7989b5d41c32d386a8a8) | fix | copy packageManager field and yarn config for temp installs |
-| [b0b3a47ae](https://github.com/angular/angular-cli/commit/b0b3a47ae95f4e98e1280ff5c9e3751ccecee5a8) | fix | support resolving subproject dependencies in pnpm workspaces |
+| [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 |
-| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- |
-| [b345bb54d](https://github.com/angular/angular-cli/commit/b345bb54d3ec7c73f9db70400f823d90aeab8fa8) | fix | remove unused spec tsconfig outDir |
+| 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 |
| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ |
-| [0c18dc0f6](https://github.com/angular/angular-cli/commit/0c18dc0f6d4f36d7bbb6f35b37bfa688e8fcf7d2) | feat | emit debug ids for stable subresource integrity hashes |
-| [291caab61](https://github.com/angular/angular-cli/commit/291caab6158fa804f6221dd024a5b96e6181eaad) | fix | prevent stripping nested sourceMappingURL comments |
+| [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 |
-
+
-# 22.0.7 (2026-07-15)
+# 20.3.34 (2026-08-12)
### @angular/cli
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ |
-| [e85c10ccb](https://github.com/angular/angular-cli/commit/e85c10ccb551eeaf9ac7e819ee6eb095317b5032) | fix | copy packageManager field and yarn config for temp installs |
-| [b87cafec0](https://github.com/angular/angular-cli/commit/b87cafec04b24c2f4337ecfa00388197ab5003f0) | fix | support resolving subproject dependencies in pnpm workspaces |
+| 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 |
-
+
-# 21.2.19 (2026-07-09)
+# 22.2.0-next.2 (2026-08-05)
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- |
+| [7e5510e464](https://github.com/angular/angular-cli/commit/7e5510e464eb423d8757ae6f058f73fc17249d81) | fix | disable linker internal source mapping in OXC linker |
+| [bad31421cb](https://github.com/angular/angular-cli/commit/bad31421cbf23339d4fbd389a91d198df405697d) | fix | prevent syntax corruption in linker and oxc transform |
+
+
+
+
+
+# 22.2.0-next.1 (2026-08-05)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| [41555dfb3b](https://github.com/angular/angular-cli/commit/41555dfb3b71d08cdfe2853bf2cbeca5b6942f67) | feat | add `--root` command line option to `mcp` command |
+| [035b9d5c9b](https://github.com/angular/angular-cli/commit/035b9d5c9bca624b98e14480f599481f294b49f3) | fix | add packages with built-in schematics to devDependencies only |
+| [0ae5690c35](https://github.com/angular/angular-cli/commit/0ae5690c35b9af00f60d4851f1bab4ba5ec8eb19) | fix | always install package during ng add to inspect manifest on disk for schematics ([#33718](https://github.com/angular/angular-cli/pull/33718)) |
+| [44ff584b33](https://github.com/angular/angular-cli/commit/44ff584b33830aa839c08feb205f014e4e7b5dca) | fix | correctly report available package updates during bare ng update |
+| [1d4f9ebbe9](https://github.com/angular/angular-cli/commit/1d4f9ebbe993a5eb8b23929a3fc55b506597e5aa) | fix | discover migrations from installed packages when omitted by registry metadata ([#33718](https://github.com/angular/angular-cli/pull/33718)) |
+| [25034f97ad](https://github.com/angular/angular-cli/commit/25034f97ad1b9a12f4080f646a0e2705425f9726) | fix | normalize Windows drive-letter casing for process.cwd |
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- |
+| [ad8271320f](https://github.com/angular/angular-cli/commit/ad8271320f67c2b1502e446f1e3cb0a05444d04f) | feat | migrate Angular Linker to oxc-parser and magic-string |
+| [4fb12292c2](https://github.com/angular/angular-cli/commit/4fb12292c21498c59836f7c80e4f824bb7751cac) | fix | prevent build process hang on unhandled error ([#33715](https://github.com/angular/angular-cli/pull/33715)) |
+| [abd8ec9f3e](https://github.com/angular/angular-cli/commit/abd8ec9f3e11f1410fcb87d0d9d60f56016912fe) | fix | prevent IIFE wrapper interleaving for adjacent classes in minified files |
+| [bcadbbef20](https://github.com/angular/angular-cli/commit/bcadbbef2054e897b9d22c1456883fc68f0383c0) | fix | retain watch files on error in load result cache |
+| [e1c7193dce](https://github.com/angular/angular-cli/commit/e1c7193dcee1c7f2feb07fdf24a6e0656f29cfaf) | fix | support case-insensitive and alternative license file names |
+| [f4a0f846e7](https://github.com/angular/angular-cli/commit/f4a0f846e7d14b5ff58c91d04461ef4a027c56b8) | perf | release build resources early in non-watch mode ([#33715](https://github.com/angular/angular-cli/pull/33715)) |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- |
+| [b74eef889f](https://github.com/angular/angular-cli/commit/b74eef889f9c8812dcbe116400663fa3d68d6b4f) | fix | settle writeResponseToNodeResponse when client disconnects |
+| [33169801c6](https://github.com/angular/angular-cli/commit/33169801c6ed7fc7c7dca2c04008e5d405374983) | perf | avoid buffering request body when sanitizing headers |
+
+
+
+
+
+# 22.1.3 (2026-08-05)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| [f837b788c6](https://github.com/angular/angular-cli/commit/f837b788c6a23e7661db5455192f5a84d20e9b37) | fix | always install package during ng add to inspect manifest on disk for schematics ([#33718](https://github.com/angular/angular-cli/pull/33718)) |
+| [72ae93638d](https://github.com/angular/angular-cli/commit/72ae93638dd83b3877773ac0d9f7e4c4e9056a33) | fix | correctly report available package updates during bare ng update |
+| [a949e06568](https://github.com/angular/angular-cli/commit/a949e0656855786e4274159be09242ff9c4eb012) | fix | discover migrations from installed packages when omitted by registry metadata ([#33718](https://github.com/angular/angular-cli/pull/33718)) |
+| [1d9122fd50](https://github.com/angular/angular-cli/commit/1d9122fd50f8a9b82199d44f9b0e075726279538) | fix | normalize Windows drive-letter casing for process.cwd |
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- |
+| [a4860d505a](https://github.com/angular/angular-cli/commit/a4860d505aff0b43fc608cd0745ed187b1d407b5) | fix | prevent build process hang on unhandled error ([#33715](https://github.com/angular/angular-cli/pull/33715)) |
+| [7686d66cab](https://github.com/angular/angular-cli/commit/7686d66cabcdfe90748ac405686d42674a19576f) | fix | retain watch files on error in load result cache |
+| [25b7cf7504](https://github.com/angular/angular-cli/commit/25b7cf7504838d541abfba043d9ea3d616a23c04) | fix | support case-insensitive and alternative license file names |
+| [4fd81402ce](https://github.com/angular/angular-cli/commit/4fd81402ce5f2f3db45a17ee0d116f12edf8d254) | perf | release build resources early in non-watch mode ([#33715](https://github.com/angular/angular-cli/pull/33715)) |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- |
+| [6af906cb94](https://github.com/angular/angular-cli/commit/6af906cb94bd0686b4c0431989e054711c74892a) | fix | settle writeResponseToNodeResponse when client disconnects |
+| [917d3b2254](https://github.com/angular/angular-cli/commit/917d3b22549c19b91e03ceb0775a3a758142b9de) | perf | avoid buffering request body when sanitizing headers |
+
+
+
+
+
+# 21.2.20 (2026-08-05)
### @angular-devkit/build-angular
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
-| [df5429871](https://github.com/angular/angular-cli/commit/df54298712bb84ec079787e4ca6c0164f6358cf1) | fix | update http-proxy-middleware to 3.0.7 |
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- |
+| [972f6db7e9](https://github.com/angular/angular-cli/commit/972f6db7e9ccf3c4b0e895341a49b104a8b0ac9b) | fix | upgrade postcss to 8.5.23 |
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- |
+| [8de75ada50](https://github.com/angular/angular-cli/commit/8de75ada503d4182af7f0be957e58fe678a63b98) | fix | upgrade postcss to 8.5.23 |
-
+
-# 20.3.32 (2026-07-09)
+# 20.3.33 (2026-08-05)
### @angular-devkit/build-angular
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
-| [d4e07cb3e](https://github.com/angular/angular-cli/commit/d4e07cb3e3f871822f9bf37fdea35814ec1cdb80) | fix | update http-proxy-middleware to 3.0.7 |
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- |
+| [0b4008f17d](https://github.com/angular/angular-cli/commit/0b4008f17d76c821d9f075b86da80b826f085d23) | fix | upgrade postcss to 8.5.23 |
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- |
+| [fc861affcd](https://github.com/angular/angular-cli/commit/fc861affcd8866b71b81c8ba90f7a63890b6577c) | fix | upgrade postcss to 8.5.23 |
-
+
+
+# 22.1.2 (2026-07-30)
-# 22.1.0-next.3 (2026-07-09)
+### @angular/cli
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- |
+| [57db27ea91](https://github.com/angular/angular-cli/commit/57db27ea910b102854267dfcd77e8fe60f23993c) | fix | add packages with built-in schematics to devDependencies only |
### @angular/build
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ |
-| [34d558c3c](https://github.com/angular/angular-cli/commit/34d558c3c98e1b59a2c2808fc04bfbb63dfe8acf) | feat | add built-in SQLite cache store fallback |
-| [52ae7f862](https://github.com/angular/angular-cli/commit/52ae7f862a581cac97d5a9b33c9b71b830d95b19) | feat | share persistent build cache across git worktrees |
-| [41bd100dd](https://github.com/angular/angular-cli/commit/41bd100dd8edebdf6c7279e1c7d3847ee5f22702) | fix | fallback to build target preserveSymlinks option in karma runner |
-| [6d9b360e5](https://github.com/angular/angular-cli/commit/6d9b360e5f2b7ab3d4ace4db35f674166630f658) | fix | prevent esbuild service hang on internal component stylesheet builds |
-| [85c53d990](https://github.com/angular/angular-cli/commit/85c53d9909b8bcb35a360044a0afde5352d45d72) | fix | prevent externalizing builder-injected i18n locale data |
-| [004cc4104](https://github.com/angular/angular-cli/commit/004cc410487b2dbb80001f045cd19f899c980ccc) | fix | strip all vite id prefixes from minified code with external dependencies |
-| [09d0a11a8](https://github.com/angular/angular-cli/commit/09d0a11a8b832f156aea100ad3497c5221fc0f50) | perf | enable fast-path AST printing with sourcemaps in AotCompilation |
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ |
+| [baed978885](https://github.com/angular/angular-cli/commit/baed978885585fceadc64f0151d7474054d57d6a) | fix | prevent IIFE wrapper interleaving for adjacent classes in minified files |
-### @angular/ssr
+
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- |
-| [0bd22dae0](https://github.com/angular/angular-cli/commit/0bd22dae09a83e36ed3863ca2b050666cc8d7aec) | fix | replace all route parameters when resolving relative redirects |
+
+
+# 22.1.1 (2026-07-30)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- |
+| [ab971a8543](https://github.com/angular/angular-cli/commit/ab971a8543d80dfc93fdddf49598564f25196160) | fix | support npm 12 metadata array and error formats |
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- |
+| [db24220fb8](https://github.com/angular/angular-cli/commit/db24220fb8efb5c9416e375a468672ff69d0659d) | fix | rewrite paths from sandboxed execroots when running under Bazel |
-
+
-# 22.0.6 (2026-07-08)
+# 22.2.0-next.0 (2026-07-30)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- |
+| [e148a8df93](https://github.com/angular/angular-cli/commit/e148a8df93b309dcd51698b4ce4f9343560edd8e) | fix | support npm 12 metadata array and error formats |
### @angular/build
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ |
-| [df49966c6](https://github.com/angular/angular-cli/commit/df49966c6b8ec0c10b58fee18981fc2992ac071a) | fix | fallback to build target preserveSymlinks option in karma runner |
-| [9906add5b](https://github.com/angular/angular-cli/commit/9906add5b722cca7f61488b367dd73545c6e13bc) | fix | prevent externalizing builder-injected i18n locale data |
-| [070155f6e](https://github.com/angular/angular-cli/commit/070155f6ead185d4b478d9034662d65b8bb86a23) | fix | strip all vite id prefixes from minified code with external dependencies |
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- |
+| [6469b5f1f5](https://github.com/angular/angular-cli/commit/6469b5f1f5946415f49000f26bddd88b5647ac3d) | fix | rewrite paths from sandboxed execroots when running under Bazel |
+
+
+
+
+
+# 22.1.0 (2026-07-29)
+
+## Deprecations
+
+### @angular-devkit/core
+
+- `stringToFileBuffer` and `fileBufferToString` are deprecated. Use standard Web APIs (`TextEncoder` and `TextDecoder`) instead.
+
+ Internal usages within the repository have been removed and replaced with standard Web APIs. The public API golden file for `@angular-devkit/core` has been updated to reflect the deprecations.
+
+### @schematics/angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- |
+| [89d7f59cd](https://github.com/angular/angular-cli/commit/89d7f59cd7cc5d821ac0e81b1fee50e27877c976) | feat | update ai-config to include Angular MCP server config |
+| [b345bb54d](https://github.com/angular/angular-cli/commit/b345bb54d3ec7c73f9db70400f823d90aeab8fa8) | fix | remove unused spec tsconfig outDir |
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- |
+| [f59bcf529](https://github.com/angular/angular-cli/commit/f59bcf52917bc5e25b97c863f96127ec8d0f12f5) | fix | abort and guide manual mitigation when updating catalog packages |
+| [48f90f5d5](https://github.com/angular/angular-cli/commit/48f90f5d5e5dd6369b2ccf044a722271c82dffd6) | fix | prevent Yarn registry environment variable override |
+| [c963ecbec](https://github.com/angular/angular-cli/commit/c963ecbec241d42dfb419ae081ba22429e37f09e) | fix | remove algoliasearch dependency and support latest docs versions |
+| [6859d25c3](https://github.com/angular/angular-cli/commit/6859d25c32b6c93cbbf2d4ad4143aa10b52b7588) | fix | resolve correct registry name when using npm alias syntax during update |
+| [3a2589dac](https://github.com/angular/angular-cli/commit/3a2589dac684dfe26b2151b940bd40280d7c5d8e) | fix | respect client-side release age settings during update resolution |
+| [5c3101bff](https://github.com/angular/angular-cli/commit/5c3101bff2e1b9d0b0e4668a183e8bcab8a1540e) | fix | revert package.json changes if installation tasks fail during update |
+| [7932caaf9](https://github.com/angular/angular-cli/commit/7932caaf987c5692d6624f6af23e65ce3f6d27fd) | fix | robustly parse npm manifest from array |
+
+### @angular-devkit/core
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------- |
+| [fd336d365](https://github.com/angular/angular-cli/commit/fd336d365dbfe8f558db177a8da24790914a541b) | refactor | deprecate stringToFileBuffer and fileBufferToString |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------- |
+| [34d558c3c](https://github.com/angular/angular-cli/commit/34d558c3c98e1b59a2c2808fc04bfbb63dfe8acf) | feat | add built-in SQLite cache store fallback |
+| [0c18dc0f6](https://github.com/angular/angular-cli/commit/0c18dc0f6d4f36d7bbb6f35b37bfa688e8fcf7d2) | feat | emit debug ids for stable subresource integrity hashes |
+| [51f69276f](https://github.com/angular/angular-cli/commit/51f69276fd2b2aab0e94901e79a51f6860ef8690) | feat | enable chunk optimization for server builds |
+| [10dc30f9c](https://github.com/angular/angular-cli/commit/10dc30f9c680f46f65b5beb030f2a75c422a3e71) | feat | migrate advanced optimization Babel plugins to oxc-parser + magic-string |
+| [917393a4c](https://github.com/angular/angular-cli/commit/917393a4cd85172574b155f727da5c1eae196fb1) | feat | migrate i18n inliner to oxc-parser + magic-string |
+| [52ae7f862](https://github.com/angular/angular-cli/commit/52ae7f862a581cac97d5a9b33c9b71b830d95b19) | feat | share persistent build cache across git worktrees |
+| [95510511d](https://github.com/angular/angular-cli/commit/95510511d963384c5a03298300246aa34a62328a) | fix | add bounded timeout to vitest executor disposal |
+| [62b11e316](https://github.com/angular/angular-cli/commit/62b11e316e8248161877592acee5954b2a8c9c66) | fix | anchor debug ID comment matching and make injection idempotent |
+| [6d9b360e5](https://github.com/angular/angular-cli/commit/6d9b360e5f2b7ab3d4ace4db35f674166630f658) | fix | prevent esbuild service hang on internal component stylesheet builds |
+| [291caab61](https://github.com/angular/angular-cli/commit/291caab6158fa804f6221dd024a5b96e6181eaad) | fix | prevent stripping nested sourceMappingURL comments |
+| [0e3ca3dfb](https://github.com/angular/angular-cli/commit/0e3ca3dfba8ef60217089d93a321660ebb6fb838) | fix | support hyphens and underscores when stripping chunk hash in chunk optimizer |
+| [585d08af8](https://github.com/angular/angular-cli/commit/585d08af881a35653dc5e7b1ee7a75b42e154517) | perf | default chunk optimization to use Rolldown |
+| [09d0a11a8](https://github.com/angular/angular-cli/commit/09d0a11a8b832f156aea100ad3497c5221fc0f50) | perf | enable fast-path AST printing with sourcemaps in AotCompilation |
### @angular/ssr
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- |
-| [c9aa354b1](https://github.com/angular/angular-cli/commit/c9aa354b1de0c946ddc994191613bb809a799f81) | fix | replace all route parameters when resolving relative redirects |
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
+| [7ef9ed24d](https://github.com/angular/angular-cli/commit/7ef9ed24d3c111474ae21c06cb2b05a21804f586) | feat | support the standard Forwarded header |
+
+
+
+
+
+# 22.0.9 (2026-07-29)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- |
+| [9ee8f9b669](https://github.com/angular/angular-cli/commit/9ee8f9b6692b8efa2d0cabd502cd5a598f436c8a) | fix | declare devserver_start and devserver_stop as isReadOnly:false |
+
+### @angular/build
+
+| Commit | Type | Description |
+| ---------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- |
+| [d0c776d461](https://github.com/angular/angular-cli/commit/d0c776d4610e2327c29900b9b3d679e9c0f28061) | fix | safeguard Karma builder stream controller against closed state |
-
+
-# 22.1.0-next.2 (2026-07-01)
+# 22.0.8 (2026-07-23)
### @angular/cli
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- |
+| [41c356256](https://github.com/angular/angular-cli/commit/41c356256525469a41ea72896b1ddff91a4e6e55) | fix | batch Prettier invocations during migration formatting |
+| [bc87c85c3](https://github.com/angular/angular-cli/commit/bc87c85c3c660e969cf77a493dc4ee58fd831214) | fix | update MCP devserver tool names to conform to spec regex |
+
+### @angular/build
+
| Commit | Type | Description |
| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- |
-| [010cef665](https://github.com/angular/angular-cli/commit/010cef6657365cad8529c164d7c594ca598180a0) | fix | gracefully handle package manager errors in command handler |
-| [3a2589dac](https://github.com/angular/angular-cli/commit/3a2589dac684dfe26b2151b940bd40280d7c5d8e) | fix | respect client-side release age settings during update resolution |
-| [edb4594de](https://github.com/angular/angular-cli/commit/edb4594deb5ff96967c1ce13b4494e3fd675ebec) | fix | respect release age policy in update bootstrapping logic |
+| [5724bf632](https://github.com/angular/angular-cli/commit/5724bf632eba59eccdd941beae8aafad35ce003e) | fix | canonicalize drive letter casing for workspace root on Windows |
+| [b8bdbaa1e](https://github.com/angular/angular-cli/commit/b8bdbaa1ea1c58f061580bd58c015078f57c1530) | fix | ensure import map integrity keys are valid URL-like specifiers |
+| [212a7bb21](https://github.com/angular/angular-cli/commit/212a7bb21eca131f32fd7b4e7b5a9ab87f2bfded) | fix | favor istanbul coverage provider when browser testing is enabled |
+| [bc760b005](https://github.com/angular/angular-cli/commit/bc760b005e0dae10a2dfd31a7f0f772482305398) | fix | preserve custom config options in runnerConfig for vitest |
+| [19cda7415](https://github.com/angular/angular-cli/commit/19cda7415693f36e911a736a32576110905b6600) | fix | remap metafile paths when workspace root is a symlink or junction |
+| [fdcc98c79](https://github.com/angular/angular-cli/commit/fdcc98c7912f45431268fdf66c8b132bd119bc27) | perf | skip semantic affected-file walk when type checking is disabled |
-### @schematics/angular
+
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- |
-| [0b3172e07](https://github.com/angular/angular-cli/commit/0b3172e076b75df5b8c3066a115d1151f8b0bad9) | fix | remove unused app tsconfig outDir |
+
+
+# 22.0.7 (2026-07-15)
+
+### @angular/cli
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ |
+| [e85c10ccb](https://github.com/angular/angular-cli/commit/e85c10ccb551eeaf9ac7e819ee6eb095317b5032) | fix | copy packageManager field and yarn config for temp installs |
+| [b87cafec0](https://github.com/angular/angular-cli/commit/b87cafec04b24c2f4337ecfa00388197ab5003f0) | fix | support resolving subproject dependencies in pnpm workspaces |
+
+
+
+
+
+# 21.2.19 (2026-07-09)
### @angular-devkit/build-angular
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ |
-| [2d3eb7fb3](https://github.com/angular/angular-cli/commit/2d3eb7fb3b43b64c476296581481dbf67cb57b1d) | fix | prevent OS command injection in ssr-dev-server builder |
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
+| [df5429871](https://github.com/angular/angular-cli/commit/df54298712bb84ec079787e4ca6c0164f6358cf1) | fix | update http-proxy-middleware to 3.0.7 |
+
+
+
+
+
+# 20.3.32 (2026-07-09)
+
+### @angular-devkit/build-angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
+| [d4e07cb3e](https://github.com/angular/angular-cli/commit/d4e07cb3e3f871822f9bf37fdea35814ec1cdb80) | fix | update http-proxy-middleware to 3.0.7 |
+
+
+
+
+
+# 22.0.6 (2026-07-08)
### @angular/build
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- |
-| [75c1dce38](https://github.com/angular/angular-cli/commit/75c1dce387075d3ea9d6da572f7daa589856bc0e) | fix | auto-inject localize/init in library unit tests |
-| [dc27e17fc](https://github.com/angular/angular-cli/commit/dc27e17fc2cbb91b10b508a7e46cd72734959ffa) | fix | inherit preserveSymlinks option from build target in unit-test builder |
-| [61e25fb1d](https://github.com/angular/angular-cli/commit/61e25fb1db56694da800f330dd65cb7e50ea2d11) | fix | load zone.js dynamically for library unit tests |
-| [0010b922f](https://github.com/angular/angular-cli/commit/0010b922f8fc5a9c57b9512594f9c3b4da3fa2ae) | fix | scope createRequire module resolution using paths to prevent parent paths |
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ |
+| [df49966c6](https://github.com/angular/angular-cli/commit/df49966c6b8ec0c10b58fee18981fc2992ac071a) | fix | fallback to build target preserveSymlinks option in karma runner |
+| [9906add5b](https://github.com/angular/angular-cli/commit/9906add5b722cca7f61488b367dd73545c6e13bc) | fix | prevent externalizing builder-injected i18n locale data |
+| [070155f6e](https://github.com/angular/angular-cli/commit/070155f6ead185d4b478d9034662d65b8bb86a23) | fix | strip all vite id prefixes from minified code with external dependencies |
### @angular/ssr
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- |
-| [d9fb854c9](https://github.com/angular/angular-cli/commit/d9fb854c9a48ba8852d57f3fa8f7a9640ee0aab2) | fix | preserve response headers during redirect |
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- |
+| [c9aa354b1](https://github.com/angular/angular-cli/commit/c9aa354b1de0c946ddc994191613bb809a799f81) | fix | replace all route parameters when resolving relative redirects |
@@ -247,43 +594,6 @@
-
-
-# 22.1.0-next.1 (2026-06-24)
-
-### @angular/cli
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- |
-| [48f90f5d5](https://github.com/angular/angular-cli/commit/48f90f5d5e5dd6369b2ccf044a722271c82dffd6) | fix | prevent Yarn registry environment variable override |
-| [38e0fabc0](https://github.com/angular/angular-cli/commit/38e0fabc05eb5d622a25c68692d7d073485750e3) | fix | support registry metadata fetching under bun package manager |
-| [3ce6e5fc3](https://github.com/angular/angular-cli/commit/3ce6e5fc3b4f4b5fb6ac99cc82023bf01894d14c) | perf | implement semaphore backpressure throttling in PackageManager |
-
-### @schematics/angular
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ |
-| [01008f1c6](https://github.com/angular/angular-cli/commit/01008f1c69321d0a8b5b8344672b594a68fe83b6) | fix | remove default workspace vscode mcp.json configuration |
-
-### @angular/build
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- |
-| [851711b03](https://github.com/angular/angular-cli/commit/851711b031603293b58e33357c70eea7675452e3) | fix | aggregate parallel worker performance timings on the main thread |
-| [585d08af8](https://github.com/angular/angular-cli/commit/585d08af881a35653dc5e7b1ee7a75b42e154517) | perf | default chunk optimization to use Rolldown |
-| [3019c8032](https://github.com/angular/angular-cli/commit/3019c8032ba9462d0edc614940b26ade24e45ec8) | perf | dispose builder result context early in non-watch mode |
-| [583736a4d](https://github.com/angular/angular-cli/commit/583736a4de8b3d0b4d79709a445809c264e38c17) | perf | implement semaphore backpressure throttling in JavaScriptTransformer |
-
-### @angular/ssr
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- |
-| [7ef9ed24d](https://github.com/angular/angular-cli/commit/7ef9ed24d3c111474ae21c06cb2b05a21804f586) | feat | support the standard Forwarded header |
-| [37f1a7491](https://github.com/angular/angular-cli/commit/37f1a749132d55a81cec231bd260ed280b10fc00) | fix | avoid caching non-SSG page lookups |
-| [5875b6024](https://github.com/angular/angular-cli/commit/5875b6024ff9e59fddf7481b6b961d73ae809963) | fix | prioritize options over environment variables in AngularNodeAppEngine |
-
-
-
# 22.0.4 (2026-06-24)
@@ -402,44 +712,6 @@
-
-
-# 22.1.0-next.0 (2026-06-11)
-
-## Deprecations
-
-### @angular-devkit/core
-
-- `stringToFileBuffer` and `fileBufferToString` are deprecated. Use standard Web APIs (`TextEncoder` and `TextDecoder`) instead.
-
- Internal usages within the repository have been removed and replaced with standard Web APIs. The public API golden file for `@angular-devkit/core` has been updated to reflect the deprecations.
-
-### @schematics/angular
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- |
-| [89d7f59cd](https://github.com/angular/angular-cli/commit/89d7f59cd7cc5d821ac0e81b1fee50e27877c976) | feat | update ai-config to include Angular MCP server config |
-
-### @angular/cli
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- |
-| [7932caaf9](https://github.com/angular/angular-cli/commit/7932caaf987c5692d6624f6af23e65ce3f6d27fd) | fix | robustly parse npm manifest from array |
-
-### @angular-devkit/core
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------- |
-| [fd336d365](https://github.com/angular/angular-cli/commit/fd336d365dbfe8f558db177a8da24790914a541b) | refactor | deprecate stringToFileBuffer and fileBufferToString |
-
-### @angular/ssr
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ |
-| [ddcef9d48](https://github.com/angular/angular-cli/commit/ddcef9d484e205c5fcfd93d184425c1c1af19c34) | fix | correct grammar in console warning for redirected location headers |
-
-
-
# 20.3.28 (2026-06-11)
diff --git a/MODULE.bazel b/MODULE.bazel
index 4b82a12dc781..9bfc8c1b806d 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.2.3")
-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_skylib", version = "1.9.0")
-bazel_dep(name = "aspect_rules_esbuild", version = "0.26.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.27.0")
bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4")
bazel_dep(name = "rules_angular")
git_override(
module_name = "rules_angular",
- commit = "db445dccfe48aad20be6ee3df456df91660ff217",
+ commit = "334ed4f0bf82cfcf21b1d8bd501d2294b186061e",
remote = "https://github.com/angular/rules_angular.git",
)
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
- commit = "daa0b5ecd15668d12163f70d4c9836ada70808c9",
+ commit = "6d918a6dd670e28afd4ba54d00b805d4aba43dca",
remote = "https://github.com/angular/dev-infra.git",
)
bazel_dep(name = "rules_browsers")
git_override(
module_name = "rules_browsers",
- commit = "3a6bd40f9be945dc51c976a21424c656448f9b09",
+ commit = "dd75910a556e309d6f475d54214551be919a21d1",
remote = "https://github.com/angular/rules_browsers.git",
)
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(
node_repositories = {
- "22.23.1-darwin_arm64": ("node-v22.23.1-darwin-arm64.tar.gz", "node-v22.23.1-darwin-arm64", "ef28d8fab2c0e4314522d4bb1b7173270aa3937e93b92cb7de79c112ac1fa953"),
- "22.23.1-darwin_amd64": ("node-v22.23.1-darwin-x64.tar.gz", "node-v22.23.1-darwin-x64", "b8da981b8a0b1241b70249204916da76c63573ddf5814dbd2d1e41069105cb81"),
- "22.23.1-linux_arm64": ("node-v22.23.1-linux-arm64.tar.xz", "node-v22.23.1-linux-arm64", "0294e8b915ab75f92c7513d2fcb830ae06e10684e6c603e99a87dbf8835389c1"),
- "22.23.1-linux_ppc64le": ("node-v22.23.1-linux-ppc64le.tar.xz", "node-v22.23.1-linux-ppc64le", "a645bcb7bbc498d09ba9249b5c6414aa6d0a461d8854afc61e32096646ba5e66"),
- "22.23.1-linux_s390x": ("node-v22.23.1-linux-s390x.tar.xz", "node-v22.23.1-linux-s390x", "f16b5636ba925b462e627c5e0ef47de3255bf4a7b38d56db58e2cf165df19c3c"),
- "22.23.1-linux_amd64": ("node-v22.23.1-linux-x64.tar.xz", "node-v22.23.1-linux-x64", "9749e988f437343b7fa832c69ded82a312e41a03116d766797ac14f6f9eee578"),
- "22.23.1-windows_amd64": ("node-v22.23.1-win-x64.zip", "node-v22.23.1-win-x64", "7df0bc9375723f4a86b3aa1b7cc73342423d9677a8df4538aca31a049e309c29"),
+ "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.1",
+ 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.15.1",
- pnpm_version_integrity = "sha512-gTULB+U8lTigLx8jA7QpD6LXvgTlbiqXDEzEtBfcdh3hlu2r1J1Vx9yVgNuBAHxEFD5OPX5GKzAA0jwlUSLQZQ==",
+ pnpm_version = "11.22.0",
+ pnpm_version_integrity = "sha512-H/hwxMYTPf2I+yr8Rt0T1H8JyXlLQ4xv20fKmMrzvBY4HuC+k6CRuOOCTPAfiJ9G19niCRD7C+GrD7W6qA3WIQ==",
)
use_repo(pnpm, "pnpm")
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index eb446604633b..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.2.3/MODULE.bazel": "3a0363a5b8ec4931488dc327c577a78c1f5a725293ebffaa5d2613283701aa8a",
- "https://bcr.bazel.build/modules/aspect_rules_js/3.2.3/source.json": "fa3187962f5fdeee0c6226792904aaafb2cf585717a264fa4393a767bffb99fe",
- "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",
@@ -69,7 +68,8 @@
"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.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0",
+ "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",
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8",
@@ -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": "DE8ssop85SA4kEHMbyc2ajgEW3TNsQFbT4L6IfuwTZo=",
- "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": "hqK7Zzp+y5jlPhecF8Z77KSrR9zpTS+H88Epy3pRc/s=",
+ "usagesDigest": "VoGyqZ+r/3juGZUfSXD4FAe965Z3XS4WANRrYH79+1Q=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {
@@ -449,13 +446,13 @@
"repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository",
"attributes": {
"deps": {
- "aspect_rules_js": "3.2.3",
- "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"
},
- "last_notice": 0
+ "last_notice": 1
}
}
},
@@ -539,7 +536,7 @@
},
"@@rules_browsers+//browsers:extensions.bzl%browsers": {
"general": {
- "bzlTransitiveDigest": "3W9m9j39szqkOoSVVqamCaSwCCw7x5Wq1kcYTwYBNOc=",
+ "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": "13d6638183ccab406ee16e28730826bd3ea57fe564a741bc935401221dfec64f",
+ "sha256": "1a20882bf1a4f9d1167171b6905efe359bb229be1ce636ae9c931f3d285fbabe",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "b50ee6ddb3bf30c91f3094fa41cc8e8de60e317c63ef058db2375346cde78d98",
+ "sha256": "1b0e0a078fc62c2b9f8cf837d41461084bd4064cf50cfc15ff84447a1f1b8dab",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "49859c1d3b0d29a82c58765e9e5fe21efa0bf7c03a86fc91eb5f3f94f34c45b6",
+ "sha256": "7438714da3696778b697b277a2df1d9701b99819ae58f5ab809b1009933677ec",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "d161d88a999effd212ce87df2ff710be1a25a495cc428d4c3029feccd02dafe9",
+ "sha256": "4349c800be1f063adc0f1a7e820d024b43ea73917a6893a3f6b3797a5200d49b",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "7bb2eedf7030523ba617704bf6c3e472b5974d048d7fa6ac6a764904556c6f36",
+ "sha256": "d275f01236831bd44c6b6712ba5bec0a028b1ce9473a6182aceae6657740a3d3",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "5c1fffef90ecea2bb0c12c5a9c0549116fc7acda13cce15881da4aa8b74e6794",
+ "sha256": "cb331889191aa8ab11be6d5a3cc4de760f8cf63dbea5d1f22c80cf16ae68e46f",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "a33d54c99a50fef25cbedb7c3d3cdd838dd784556138648bcc5da5814109a306",
+ "sha256": "1584dc9ae35f4a22c4ba4b142335d91d6d3096813395d3acaf135fb6e716ce8e",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "2673d6a87dabd50c516d4d69b4daf742ff6169877c04bb226971503012262d8c",
+ "sha256": "061b4d1be9eb2a11ed036b3e562b565ebfd257fcd7cda3681a58e63bff5dd6d0",
"urls": [
- "https://storage.googleapis.com/chrome-for-testing-public/152.0.7960.0/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": "748704b06ffaabc8bd2b3351c93c244db4e29bfe21af6ecb4d6c124c7bd4a234",
+ "sha256": "7665cd49ab13417270748325838e565136adbc76d41bbd76fb24d15a0cc7792b",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/152.0/linux-x86_64/en-US/firefox-152.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": "b8a46188850d2fb32f16ad3c0829b08cd689bc714b8ee18fd9b09bb60629004d",
+ "sha256": "b0295d3b77ec632a60282cf2b9770e1dada085879d00e43d8d23fddf5015e06a",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/152.0/mac/en-US/Firefox%20152.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": "b8a46188850d2fb32f16ad3c0829b08cd689bc714b8ee18fd9b09bb60629004d",
+ "sha256": "b0295d3b77ec632a60282cf2b9770e1dada085879d00e43d8d23fddf5015e06a",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/152.0/mac/en-US/Firefox%20152.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": "3c426623ff83cb014402607c7ec99fea5b39eec2e44a07f639680c393ff9784e",
+ "sha256": "6b3c996d27b50dca8e5b6dd2956233cc6bf4720cea3f0847a9423c2cd6759e73",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/152.0/win64/en-US/Firefox%20Setup%20152.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": "Sllnnruao+9gE+ivwtmfqL78yZPGXm4HVMOBoVEIW0U=",
+ "usagesDigest": "9kbJ6OfvQpAM3otgkGgS5xpOHOoVgV5ZJ9jSgMgUG2g=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
diff --git a/constants.bzl b/constants.bzl
index 6987637a4442..143ee7243735 100644
--- a/constants.bzl
+++ b/constants.bzl
@@ -3,10 +3,10 @@ RELEASE_ENGINES_NODE = "^22.22.3 || ^24.15.0 || >=26.0.0"
RELEASE_ENGINES_NPM = "^6.11.0 || ^7.5.6 || >=8.0.0"
RELEASE_ENGINES_YARN = ">= 1.13.0"
-NG_PACKAGR_VERSION = "^22.1.0-next.0"
-ANGULAR_FW_VERSION = "^22.1.0-next.0"
-ANGULAR_FW_PEER_DEP = "^22.0.0 || ^22.1.0-next.0"
-NG_PACKAGR_PEER_DEP = "^22.0.0 || ^22.1.0-next.0"
+NG_PACKAGR_VERSION = "^22.2.0-next.0"
+ANGULAR_FW_VERSION = "^22.2.0-next.0"
+ANGULAR_FW_PEER_DEP = "^22.0.0 || ^22.2.0-next.0"
+NG_PACKAGR_PEER_DEP = "^22.0.0 || ^22.2.0-next.0"
# Baseline widely-available date in `YYYY-MM-DD` format which defines Angular's
# browser support. This date serves as the source of truth for the Angular CLI's
diff --git a/goldens/public-api/angular_devkit/build_webpack/index.api.md b/goldens/public-api/angular_devkit/build_webpack/index.api.md
index abe0725a36fe..3e7ebf07873b 100644
--- a/goldens/public-api/angular_devkit/build_webpack/index.api.md
+++ b/goldens/public-api/angular_devkit/build_webpack/index.api.md
@@ -6,6 +6,7 @@
import { BuilderContext } from '@angular-devkit/architect';
import { BuilderOutput } from '@angular-devkit/architect';
+import type { Configuration } from 'webpack-dev-server';
import { Observable } from 'rxjs';
import type webpack from 'webpack';
import type WebpackDevServer from 'webpack-dev-server';
@@ -50,7 +51,7 @@ export function runWebpack(config: webpack.Configuration, context: BuilderContex
// @public @deprecated (undocumented)
export function runWebpackDevServer(config: webpack.Configuration, context: BuilderContext, options?: {
shouldProvideStats?: boolean;
- devServerConfig?: WebpackDevServer.Configuration;
+ devServerConfig?: Configuration;
logging?: WebpackLoggingCallback;
webpackFactory?: WebpackFactory;
webpackDevServerFactory?: WebpackDevServerFactory;
diff --git a/modules/testing/builder/package.json b/modules/testing/builder/package.json
index 9aede9dc7d10..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": "29.1.1",
- "ng-packagr": "22.1.0-next.3",
+ "jsdom": "30.0.1",
+ "ng-packagr": "22.2.0-next.3",
"rxjs": "7.8.2",
- "vitest": "4.1.10"
+ "vitest": "4.1.11"
}
}
diff --git a/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json b/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json
index 0ec9802c2c70..c8105b2c16b4 100644
--- a/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json
+++ b/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json
@@ -2,7 +2,7 @@
"name": "lib",
"version": "0.0.1",
"peerDependencies": {
- "@angular/common": "^22.1.0-next",
- "@angular/core": "^22.1.0-next"
+ "@angular/common": "^22.2.0-next",
+ "@angular/core": "^22.2.0-next"
}
}
\ No newline at end of file
diff --git a/package.json b/package.json
index f67f33227c76..df3a9c868816 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@angular/devkit-repo",
- "version": "22.1.0-next.4",
+ "version": "22.2.0-next.4",
"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.15.1",
+ "packageManager": "pnpm@11.22.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.15.1"
+ "pnpm": "11.22.0"
},
"author": "Angular Authors",
"license": "MIT",
@@ -42,23 +42,23 @@
},
"homepage": "https://github.com/angular/angular-cli",
"dependencies": {
- "@angular/compiler-cli": "22.1.0-rc.0",
+ "@angular/compiler-cli": "22.2.0-next.3",
"typescript": "6.0.3"
},
"devDependencies": {
- "@angular/animations": "22.1.0-rc.0",
- "@angular/cdk": "22.1.0-next.4",
- "@angular/common": "22.1.0-rc.0",
- "@angular/compiler": "22.1.0-rc.0",
- "@angular/core": "22.1.0-rc.0",
- "@angular/forms": "22.1.0-rc.0",
- "@angular/localize": "22.1.0-rc.0",
- "@angular/material": "22.1.0-next.4",
- "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#7a67d5221a9efec2b69b102759d04223a3fc8552",
- "@angular/platform-browser": "22.1.0-rc.0",
- "@angular/platform-server": "22.1.0-rc.0",
- "@angular/router": "22.1.0-rc.0",
- "@angular/service-worker": "22.1.0-rc.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.2",
+ "@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.64.0",
- "@typescript-eslint/parser": "8.64.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.7.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.7.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.0.0",
+ "magic-string": "1.2.2",
"prettier": "^3.0.0",
- "puppeteer": "25.3.0",
+ "puppeteer": "25.8.0",
"quicktype-core": "26.0.0",
- "rollup": "4.62.2",
+ "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.7.0",
+ "undici": "8.10.0",
"unenv": "^1.10.0",
- "verdaccio": "6.8.0",
+ "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 7325ec88d35b..75eedd6a7f5f 100644
--- a/packages/angular/build/BUILD.bazel
+++ b/packages/angular/build/BUILD.bazel
@@ -82,13 +82,13 @@ ts_project(
":node_modules/@angular-devkit/core",
":node_modules/@angular/ssr",
":node_modules/@babel/core",
- ":node_modules/@babel/helper-annotate-as-pure",
- ":node_modules/@babel/helper-split-export-declaration",
":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",
@@ -110,7 +110,7 @@ ts_project(
":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",
@@ -125,7 +125,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",
@@ -146,6 +145,7 @@ ts_project(
":build",
":node_modules/@angular-devkit/core",
":node_modules/@babel/core",
+ ":node_modules/oxc-parser",
"//:node_modules/@angular/compiler-cli",
"//:node_modules/@types/jasmine",
"//:node_modules/esbuild",
@@ -338,7 +338,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 a049f92d4a73..0a998207cf27 100644
--- a/packages/angular/build/package.json
+++ b/packages/angular/build/package.json
@@ -21,29 +21,29 @@
"@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER",
"@babel/core": "8.0.1",
- "@babel/helper-annotate-as-pure": "8.0.0",
- "@babel/helper-split-export-declaration": "7.24.7",
- "@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": "10.2.2",
- "magic-string": "1.0.0",
+ "listr2": "11.0.0",
+ "magic-string": "1.2.2",
"mrmime": "2.0.1",
- "oxc-parser": "0.140.0",
+ "oxc-parser": "0.146.0",
"parse5-html-rewriting-stream": "8.0.1",
"picomatch": "4.0.5",
- "piscina": "5.2.0",
- "rolldown": "1.2.0",
- "sass": "1.101.0",
+ "piscina": "5.3.1",
+ "rolldown": "1.2.5",
+ "sass": "1.103.1",
"semver": "7.8.5",
"source-map-support": "0.5.21",
"tinyglobby": "0.2.17",
- "vite": "8.1.5",
- "watchpack": "2.5.2"
+ "vite": "8.2.2",
+ "xxhash-wasm": "1.1.0"
},
"optionalDependencies": {
"lmdb": "3.5.6"
@@ -51,15 +51,15 @@
"devDependencies": {
"@angular-devkit/core": "workspace:*",
"@angular/ssr": "workspace:*",
- "@oxc-project/types": "0.140.0",
+ "@oxc-project/types": "0.146.0",
"istanbul-lib-instrument": "6.0.3",
- "jsdom": "29.1.1",
- "less": "4.6.7",
- "ng-packagr": "22.1.0-next.3",
- "postcss": "8.5.19",
- "rollup": "4.62.2",
+ "jsdom": "30.0.1",
+ "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 d7dbdc8cfe21..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,10 +80,12 @@ export async function* runEsBuildBuildAction(
incrementalResults,
} = options;
+ await initializeHash();
+
const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;
// Initial build
- let result: ExecutionResult;
+ let result: ExecutionResult | undefined;
try {
// Perform the build action
result = await withProgress('Building...', () => action());
@@ -89,8 +93,8 @@ export async function* runEsBuildBuildAction(
// Log all diagnostic (error/warning/logs) messages
await logMessages(logger, result, colors, jsonLogs);
} finally {
- // Ensure Sass workers are shutdown if not watching
- if (!watch) {
+ // Ensure Sass workers are shutdown if not watching or if the initial build failed
+ if (!watch || !result) {
shutdownSassWorkerPool();
}
}
@@ -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 21beb57bf478..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,15 +75,22 @@ 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;
try {
if (rebuildState) {
bundlerContexts = rebuildState.rebuildContexts;
componentStyleBundler = rebuildState.componentStyleBundler;
codeBundleCache = rebuildState.codeBundleCache;
templateUpdates = rebuildState.templateUpdates;
+ executionResult = new ExecutionResult(
+ bundlerContexts,
+ componentStyleBundler,
+ codeBundleCache,
+ templateUpdates,
+ );
// Reset template updates for new rebuild
templateUpdates?.clear();
@@ -87,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,
);
@@ -101,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);
@@ -122,9 +134,15 @@ export async function executeBuild(
angularCompilationContext,
templateUpdates,
);
+ executionResult = new ExecutionResult(
+ bundlerContexts,
+ componentStyleBundler,
+ codeBundleCache,
+ templateUpdates,
+ );
// Bundle everything on initial build
- bundlingResult = await BundlerContext.bundleAll([
+ bundlingIndividualResults = await BundlerContext.bundleAll([
...bundlerContexts.typescriptContexts,
...bundlerContexts.otherContexts,
]);
@@ -136,236 +154,278 @@ export async function executeBuild(
componentStyleBundler.invalidate(rebuildState.fileChanges.all);
const componentResults = await componentStyleBundler.bundleAllFiles(true, true);
- bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
+ bundlingIndividualResults.push(...componentResults);
}
- } catch (error) {
- await angularCompilationContext?.dispose();
- throw error;
- }
-
- const executionResult = new ExecutionResult(
- bundlerContexts,
- componentStyleBundler,
- codeBundleCache,
- templateUpdates,
- );
- executionResult.addWarnings(bundlingResult.warnings);
-
- // Add used external component style referenced files to be watched
- if (options.externalRuntimeStyles) {
- executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles());
- }
- // Return if the bundling has errors
- if (bundlingResult.errors) {
- executionResult.addErrors(bundlingResult.errors);
+ let bundlingResult = BundlerContext.mergeResults(bundlingIndividualResults);
- return executionResult;
- }
+ executionResult.addWarnings(bundlingResult.warnings);
- // Optimize chunks if enabled and threshold is met.
- // This pass uses Rollup/Rolldown to further optimize chunks generated by esbuild.
- 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(
- (path) => path.endsWith('.js') && !initialFiles.has(path),
- ).length;
-
- // Only run if the number of lazy chunks meets the configured threshold.
- // This avoids overhead for small projects with few chunks.
- if (lazyChunksCount >= optimizeChunksThreshold) {
- const { optimizeChunks } = await import('./chunk-optimizer');
- const optimizationResult = await profileAsync('OPTIMIZE_CHUNKS', () =>
- optimizeChunks(
- bundlingResult,
- options.sourcemapOptions.scripts ? !options.sourcemapOptions.hidden || 'hidden' : false,
- ),
- );
+ // Add used external component style referenced files to be watched
+ if (options.externalRuntimeStyles) {
+ executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles());
+ }
- if (optimizationResult.errors) {
- executionResult.addErrors(optimizationResult.errors);
+ // In non-watch mode, the bundler rebuild contexts and Angular compilation
+ // are no longer needed once bundling completes. Dispose them early to free
+ // worker processes, TypeScript ASTs, and caches before post-bundling steps
+ // execute or before returning bundling errors.
+ // This is fire-and-forget so worker teardown runs concurrently in the background
+ // without blocking the critical path of post-bundle optimization steps.
+ // Any in-flight disposal is awaited in the builder action's finally block.
+ if (!options.watch) {
+ void angularCompilationContext?.dispose();
+ void executionResult.dispose();
+ }
- return executionResult;
+ // 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,
+ });
+ }
+ }
}
- bundlingResult = optimizationResult;
+ executionResult.addErrors(bundlingResult.errors);
+
+ return executionResult;
}
- }
- // Analyze external imports if external options are enabled
- if (options.externalPackages || bundlingResult.externalConfiguration) {
- const {
- externalConfiguration = [],
- externalImports: { browser = [], server = [] },
- } = bundlingResult;
- // Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*,
- // which matches import paths like @foo/bar/baz.
- // This means all paths within the @foo/bar package are also marked as external.
- const exclusionsPrefixes = externalConfiguration.map((exclusion) => exclusion + '/');
- const exclusions = new Set(externalConfiguration);
- const explicitExternal = new Set();
-
- const isExplicitExternal = (dep: string): boolean => {
- // Locale-data imports are injected by the builder itself (see i18n-locale-plugin) and must stay resolvable
- // even when the containing package is externalized by the user.
- if (dep.startsWith(`${LOCALE_DATA_BASE_MODULE}/`)) {
- return false;
- }
+ // Optimize chunks if enabled and threshold is met.
+ // This pass uses Rollup/Rolldown to further optimize chunks generated by esbuild.
+ 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 { metafiles, initialFiles } = bundlingResult;
+ const lazyChunksCount = Object.keys(metafiles.browser.outputs || {}).filter(
+ (path) => path.endsWith('.js') && !initialFiles.has(path),
+ ).length;
+
+ // Only run if the number of lazy chunks meets the configured threshold.
+ // This avoids overhead for small projects with few chunks.
+ if (lazyChunksCount >= optimizeChunksThreshold) {
+ const { optimizeChunks } = await import('./chunk-optimizer');
+ const optimizationResult = await profileAsync('OPTIMIZE_CHUNKS', () =>
+ optimizeChunks(
+ bundlingResult,
+ options.sourcemapOptions.scripts ? !options.sourcemapOptions.hidden || 'hidden' : false,
+ ),
+ );
+
+ if (optimizationResult.errors) {
+ executionResult.addErrors(optimizationResult.errors);
+
+ return executionResult;
+ }
- if (exclusions.has(dep)) {
- return true;
+ bundlingResult = optimizationResult;
}
+ }
+
+ // Analyze external imports if external options are enabled
+ if (options.externalPackages || bundlingResult.externalConfiguration) {
+ const {
+ externalConfiguration = [],
+ externalImports: { browser = [], server = [] },
+ } = bundlingResult;
+ // Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*,
+ // which matches import paths like @foo/bar/baz.
+ // This means all paths within the @foo/bar package are also marked as external.
+ const exclusionsPrefixes = externalConfiguration.map((exclusion) => exclusion + '/');
+ const exclusions = new Set(externalConfiguration);
+ const explicitExternal = new Set();
+
+ const isExplicitExternal = (dep: string): boolean => {
+ // Locale-data imports are injected by the builder itself (see i18n-locale-plugin) and must stay resolvable
+ // even when the containing package is externalized by the user.
+ if (dep.startsWith(`${LOCALE_DATA_BASE_MODULE}/`)) {
+ return false;
+ }
- for (const prefix of exclusionsPrefixes) {
- if (dep.startsWith(prefix)) {
+ if (exclusions.has(dep)) {
return true;
}
- }
- return false;
- };
+ for (const prefix of exclusionsPrefixes) {
+ if (dep.startsWith(prefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ const implicitBrowser: string[] = [];
+ for (const dep of browser) {
+ if (isExplicitExternal(dep)) {
+ explicitExternal.add(dep);
+ } else {
+ implicitBrowser.push(dep);
+ }
+ }
- const implicitBrowser: string[] = [];
- for (const dep of browser) {
- if (isExplicitExternal(dep)) {
- explicitExternal.add(dep);
- } else {
- implicitBrowser.push(dep);
+ const implicitServer: string[] = [];
+ for (const dep of server) {
+ if (isExplicitExternal(dep)) {
+ explicitExternal.add(dep);
+ } else {
+ implicitServer.push(dep);
+ }
}
+
+ executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]);
}
- const implicitServer: string[] = [];
- for (const dep of server) {
- if (isExplicitExternal(dep)) {
- explicitExternal.add(dep);
- } else {
- implicitServer.push(dep);
+ 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(browserMetafile, outputFiles, initialFiles);
+ budgetFailures = [...checkBudgets(options.budgets, compatStats, true)];
+ for (const { message, severity } of budgetFailures) {
+ if (severity === 'error') {
+ executionResult.addError(message);
+ } else {
+ executionResult.addWarning(message);
+ }
}
}
- executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]);
- }
+ // Calculate estimated transfer size if scripts are optimized
+ let estimatedTransferSizes;
+ if (optimizationOptions.scripts || optimizationOptions.styles.minify) {
+ estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles);
+ }
- const { metafile, initialFiles, outputFiles } = bundlingResult;
+ // Check metafile for CommonJS module usage if optimizing scripts
+ if (optimizationOptions.scripts) {
+ const messages = checkCommonJSModules(browserMetafile, options.allowedCommonJsDependencies);
+ executionResult.addWarnings(messages);
+ }
- executionResult.outputFiles.push(...outputFiles);
+ // Copy assets
+ if (assets) {
+ executionResult.addAssets(await resolveAssets(assets, workspaceRoot));
+ }
- // Analyze files for bundle budget failures if present
- let budgetFailures: BudgetCalculatorResult[] | undefined;
- if (options.budgets) {
- const compatStats = generateBudgetStats(metafile, outputFiles, initialFiles);
- budgetFailures = [...checkBudgets(options.budgets, compatStats, true)];
- for (const { message, severity } of budgetFailures) {
- if (severity === 'error') {
- executionResult.addError(message);
- } else {
- executionResult.addWarning(message);
- }
+ // Extract and write licenses for used packages
+ if (options.extractLicenses) {
+ executionResult.addOutputFile(
+ '3rdpartylicenses.txt',
+ await extractLicenses(metafiles, workspaceRoot),
+ BuildOutputFileType.Root,
+ );
}
- }
- // Calculate estimated transfer size if scripts are optimized
- let estimatedTransferSizes;
- if (optimizationOptions.scripts || optimizationOptions.styles.minify) {
- estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles);
- }
+ // Watch input index HTML file if configured
+ if (options.indexHtmlOptions) {
+ executionResult.extraWatchFiles.push(options.indexHtmlOptions.input);
+ executionResult.htmlIndexPath = options.indexHtmlOptions.output;
+ executionResult.htmlBaseHref = options.baseHref;
+ }
- // Check metafile for CommonJS module usage if optimizing scripts
- if (optimizationOptions.scripts) {
- const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies);
- executionResult.addWarnings(messages);
- }
+ // Create server app engine manifest
+ if (serverEntryPoint) {
+ executionResult.addOutputFile(
+ SERVER_APP_ENGINE_MANIFEST_FILENAME,
+ generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref),
+ BuildOutputFileType.ServerRoot,
+ );
+ }
- // Copy assets
- if (assets) {
- executionResult.addAssets(await resolveAssets(assets, workspaceRoot));
- }
+ // Perform i18n translation inlining if enabled
+ if (i18nOptions.shouldInline) {
+ 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(
+ browserMetafile,
+ options,
+ executionResult.outputFiles,
+ executionResult.assetFiles,
+ initialFiles,
+ // Set lang attribute to the defined source locale if present
+ i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
+ );
- // Extract and write licenses for used packages
- if (options.extractLicenses) {
- executionResult.addOutputFile(
- '3rdpartylicenses.txt',
- await extractLicenses(metafile, workspaceRoot),
- BuildOutputFileType.Root,
- );
- }
+ // Deduplicate and add errors and warnings
+ executionResult.addErrors([...new Set(result.errors)]);
+ executionResult.addWarnings([...new Set(result.warnings)]);
- // Watch input index HTML file if configured
- if (options.indexHtmlOptions) {
- executionResult.extraWatchFiles.push(options.indexHtmlOptions.input);
- executionResult.htmlIndexPath = options.indexHtmlOptions.output;
- executionResult.htmlBaseHref = options.baseHref;
- }
+ executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
+ executionResult.outputFiles.push(...result.additionalOutputFiles);
+ executionResult.assetFiles.push(...result.additionalAssets);
+ }
- // Create server app engine manifest
- if (serverEntryPoint) {
- executionResult.addOutputFile(
- SERVER_APP_ENGINE_MANIFEST_FILENAME,
- generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref),
- BuildOutputFileType.ServerRoot,
- );
- }
+ if (serverEntryPoint) {
+ executionResult.addOutputFile(
+ 'prerendered-routes.json',
+ JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2),
+ BuildOutputFileType.Root,
+ );
+ }
- // Perform i18n translation inlining if enabled
- if (i18nOptions.shouldInline) {
- const result = await inlineI18n(metafile, options, executionResult, initialFiles);
- executionResult.addErrors(result.errors);
- executionResult.addWarnings(result.warnings);
- executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
- } else {
- const result = await executePostBundleSteps(
- metafile,
- options,
- executionResult.outputFiles,
- executionResult.assetFiles,
- initialFiles,
- // Set lang attribute to the defined source locale if present
- i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
- );
+ // Write metafiles if stats option is enabled
+ if (options.stats) {
+ executionResult.addOutputFile(
+ 'browser-stats.json',
+ JSON.stringify(browserMetafile, null, 2),
+ BuildOutputFileType.Root,
+ );
- // Deduplicate and add errors and warnings
- executionResult.addErrors([...new Set(result.errors)]);
- executionResult.addWarnings([...new Set(result.warnings)]);
+ if (serverEntryPoint) {
+ executionResult.addOutputFile(
+ 'server-stats.json',
+ JSON.stringify(serverMetafile, null, 2),
+ BuildOutputFileType.Root,
+ );
+ }
+ }
- executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
- executionResult.outputFiles.push(...result.additionalOutputFiles);
- executionResult.assetFiles.push(...result.additionalAssets);
- }
+ if (!jsonLogs && !options.quiet) {
+ const changedFiles =
+ rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
+ executionResult.addLog(
+ logBuildStats(
+ metafiles,
+ outputFiles,
+ initialFiles,
+ budgetFailures,
+ colors,
+ changedFiles,
+ estimatedTransferSizes,
+ !!ssrOptions,
+ verbose,
+ ),
+ );
+ }
- executionResult.addOutputFile(
- 'prerendered-routes.json',
- JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2),
- BuildOutputFileType.Root,
- );
-
- // Write metafile if stats option is enabled
- if (options.stats) {
- executionResult.addOutputFile(
- 'stats.json',
- JSON.stringify(metafile, null, 2),
- BuildOutputFileType.Root,
- );
- }
+ return executionResult;
+ } catch (error) {
+ await Promise.allSettled([angularCompilationContext?.dispose(), executionResult?.dispose()]);
- if (!jsonLogs && !options.quiet) {
- const changedFiles =
- rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
- executionResult.addLog(
- logBuildStats(
- metafile,
- outputFiles,
- initialFiles,
- budgetFailures,
- colors,
- changedFiles,
- estimatedTransferSizes,
- !!ssrOptions,
- verbose,
- ),
- );
+ throw error;
}
-
- return executionResult;
}
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 4ec810ff3c07..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. */
@@ -160,7 +174,7 @@ export async function normalizeOptions(
options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks');
// Setup base paths based on workspace root and project information
- const workspaceRoot = canonicalizePath(context.workspaceRoot, preserveSymlinks);
+ const workspaceRoot = canonicalizePath(context.workspaceRoot);
const projectMetadata = await context.getProjectMetadata(projectName);
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);
@@ -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/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts
index 22c4c32202bd..2de5c5f229ca 100644
--- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts
+++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts
@@ -132,5 +132,119 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
{ outputLogsOnFailure: false },
);
});
+
+ it('rebuilds PostCSS stylesheet after error on rebuild from plugin dependency', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ watch: true,
+ styles: ['src/styles.css'],
+ });
+
+ await harness.writeFile(
+ 'test-plugin.js',
+ `
+ const fs = require('fs');
+ const path = require('path');
+ module.exports = () => {
+ return {
+ postcssPlugin: 'test-plugin',
+ Once(root, { result }) {
+ const themePath = path.join(path.dirname(root.source.input.file), 'theme.json');
+ result.messages.push({
+ type: 'dependency',
+ file: themePath,
+ });
+ const data = fs.readFileSync(themePath, 'utf-8');
+ const json = JSON.parse(data);
+ root.append('body { color: ' + json.color + '; }');
+ },
+ };
+ };
+ module.exports.postcss = true;
+ `,
+ );
+ await harness.writeFile(
+ '.postcssrc.json',
+ JSON.stringify({
+ plugins: {
+ './test-plugin.js': {},
+ },
+ }),
+ );
+ await harness.writeFile('src/styles.css', '/* base */');
+ await harness.writeFile('src/theme.json', '{"color": "aqua"}');
+
+ await harness.executeWithCases(
+ [
+ async ({ result }) => {
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua');
+ harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue');
+
+ await harness.writeFile('src/theme.json', 'invalid-json');
+ },
+ async ({ result }) => {
+ expect(result?.success).toBe(false);
+
+ await harness.writeFile('src/theme.json', '{"color": "blue"}');
+ },
+ ({ result }) => {
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua');
+ harness.expectFile('dist/browser/styles.css').content.toContain('color: blue');
+ },
+ ],
+ { outputLogsOnFailure: false },
+ );
+ });
+
+ it('rebuilds PostCSS stylesheet after CSS syntax error on initial build from import', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ watch: true,
+ styles: ['src/styles.css'],
+ });
+
+ await harness.writeFile(
+ 'noop-plugin.js',
+ `
+ module.exports = () => ({ postcssPlugin: 'noop-plugin' });
+ module.exports.postcss = true;
+ `,
+ );
+ await harness.writeFile(
+ '.postcssrc.json',
+ JSON.stringify({
+ plugins: {
+ './noop-plugin.js': {},
+ },
+ }),
+ );
+ await harness.writeFile('src/styles.css', "@import './a.css';");
+ await harness.writeFile('src/a.css', "a { ' }");
+
+ await harness.executeWithCases(
+ [
+ async ({ result }) => {
+ expect(result?.success).toBe(false);
+
+ await harness.writeFile('src/a.css', 'body { color: aqua; }');
+ },
+ async ({ result }) => {
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua');
+ harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue');
+
+ await harness.writeFile('src/a.css', 'body { color: blue; }');
+ },
+ ({ result }) => {
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua');
+ harness.expectFile('dist/browser/styles.css').content.toContain('color: blue');
+ },
+ ],
+ { outputLogsOnFailure: false },
+ );
+ });
});
});
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/extract-licenses_spec.ts b/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts
index 402200a27f9d..2c495e703a7b 100644
--- a/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts
+++ b/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts
@@ -55,5 +55,96 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('MIT');
harness.expectFile('dist/browser/en-US/main.js').toExist();
});
+
+ it(`should extract license from a package with a lowercase 'license' file`, async () => {
+ await harness.writeFile(
+ 'node_modules/test-package-a/package.json',
+ JSON.stringify({
+ name: 'test-package-a',
+ version: '1.0.0',
+ main: 'index.js',
+ license: 'MIT',
+ }),
+ );
+ await harness.writeFile(
+ 'node_modules/test-package-a/index.js',
+ 'console.log("test-package-a");',
+ );
+ await harness.writeFile('node_modules/test-package-a/license', 'TEST_LOWERCASE_LICENSE_TEXT');
+ await harness.appendToFile('src/main.ts', "\nimport 'test-package-a';\n");
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ extractLicenses: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+ harness
+ .expectFile('dist/3rdpartylicenses.txt')
+ .content.toContain('TEST_LOWERCASE_LICENSE_TEXT');
+ });
+
+ it(`should extract license from a package with an alternative license file name (e.g., 'MIT-LICENCE.txt')`, async () => {
+ await harness.writeFile(
+ 'node_modules/test-package-b/package.json',
+ JSON.stringify({
+ name: 'test-package-b',
+ version: '1.0.0',
+ main: 'index.js',
+ license: 'MIT',
+ }),
+ );
+ await harness.writeFile(
+ 'node_modules/test-package-b/index.js',
+ 'console.log("test-package-b");',
+ );
+ await harness.writeFile(
+ 'node_modules/test-package-b/MIT-LICENCE.txt',
+ 'TEST_ALTERNATIVE_LICENSE_TEXT',
+ );
+ await harness.appendToFile('src/main.ts', "\nimport 'test-package-b';\n");
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ extractLicenses: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+ harness
+ .expectFile('dist/3rdpartylicenses.txt')
+ .content.toContain('TEST_ALTERNATIVE_LICENSE_TEXT');
+ });
+
+ it(`should extract license from a package with a custom license file specified in package.json`, async () => {
+ await harness.writeFile(
+ 'node_modules/test-package-c/package.json',
+ JSON.stringify({
+ name: 'test-package-c',
+ version: '1.0.0',
+ main: 'index.js',
+ license: 'SEE LICENSE IN custom-license.md',
+ }),
+ );
+ await harness.writeFile(
+ 'node_modules/test-package-c/index.js',
+ 'console.log("test-package-c");',
+ );
+ await harness.writeFile(
+ 'node_modules/test-package-c/custom-license.md',
+ 'TEST_CUSTOM_LICENSE_TEXT',
+ );
+ await harness.appendToFile('src/main.ts', "\nimport 'test-package-c';\n");
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ extractLicenses: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBeTrue();
+ harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('TEST_CUSTOM_LICENSE_TEXT');
+ });
});
});
diff --git a/packages/angular/build/src/builders/application/tests/options/index_spec.ts b/packages/angular/build/src/builders/application/tests/options/index_spec.ts
index 11228658bbce..b53467caaec6 100644
--- a/packages/angular/build/src/builders/application/tests/options/index_spec.ts
+++ b/packages/angular/build/src/builders/application/tests/options/index_spec.ts
@@ -79,16 +79,17 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/index.html').content.toContain('TEST_123');
});
- // TODO: Build needs to be fixed to not throw an unhandled exception for this case
- xit('should fail build when a string path to non-existent file', async () => {
+ it('should fail build and terminate cleanly when a string path to non-existent file is provided', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: 'src/not-here.html',
});
- const { result } = await harness.executeOnce({ outputLogsOnFailure: false });
+ const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
- expect(result?.success).toBe(false);
+ expect(result).toBeUndefined();
+ expect(error).toEqual(jasmine.any(Error));
+ expect(error?.message).toMatch(/Failed to read index HTML file/i);
harness.expectFile('dist/browser/index.html').toNotExist();
});
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/server.ts b/packages/angular/build/src/builders/dev-server/vite/server.ts
index c616e865a68b..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';
@@ -104,6 +105,7 @@ function createSsrConfig(
prebundleLoaderExtensions: RolldownLoaderOption | undefined,
thirdPartySourcemaps: boolean,
define: ApplicationBuilderInternalOptions['define'],
+ target: string[],
): Vite.SSROptions {
return {
// Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored.
@@ -111,6 +113,7 @@ function createSsrConfig(
// Exclude any Node.js built in module and provided dependencies (currently build defined externals)
external: externalMetadata.explicitServer,
optimizeDeps: getDepOptimizationConfig({
+ target,
// Only enable with caching since it causes prebundle dependencies to be cached
disabled: serverOptions.prebundle === false,
// Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins)
@@ -145,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
@@ -214,6 +218,7 @@ export async function setupServer(
prebundleLoaderExtensions,
thirdPartySourcemaps,
define,
+ target,
),
plugins: [
createAngularSetupMiddlewaresPlugin({
@@ -239,6 +244,7 @@ export async function setupServer(
],
// Browser only optimizeDeps. (This does not run for SSR dependencies).
optimizeDeps: getDepOptimizationConfig({
+ target,
// Only enable with caching since it causes prebundle dependencies to be cached
disabled: serverOptions.prebundle === false,
// Exclude any explicitly defined dependencies (currently build defined externals)
diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts
index b9a28e1bf0b0..d0adb6c3cf0e 100644
--- a/packages/angular/build/src/builders/karma/application_builder.ts
+++ b/packages/angular/build/src/builders/karma/application_builder.ts
@@ -11,7 +11,6 @@ import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from
import { randomUUID } from 'node:crypto';
import { rmSync } from 'node:fs';
import * as fs from 'node:fs/promises';
-import { createRequire } from 'node:module';
import path from 'node:path';
import { ReadableStream } from 'node:stream/web';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
@@ -66,8 +65,14 @@ export function execute(
init = await initializeApplication(normalizedOptions, context, karmaOptions, transforms);
} catch (err) {
if (err instanceof ApplicationBuildError) {
- controller.enqueue({ success: false, message: err.message });
- controller.close();
+ if (controller.desiredSize !== null) {
+ try {
+ controller.enqueue({ success: false, message: err.message });
+ controller.close();
+ } catch {
+ // Stream controller may already be closed or cancelled
+ }
+ }
return;
}
@@ -85,8 +90,14 @@ export function execute(
// Close the stream once the Karma server returns.
karmaServer = new karma.Server(karmaConfig as Config, (exitCode) => {
- controller.enqueue({ success: exitCode === 0 });
- controller.close();
+ if (controller.desiredSize !== null) {
+ try {
+ controller.enqueue({ success: exitCode === 0 });
+ controller.close();
+ } catch {
+ // Stream controller may already be closed or cancelled
+ }
+ }
});
await karmaServer.start();
diff --git a/packages/angular/build/src/builders/karma/progress-reporter.ts b/packages/angular/build/src/builders/karma/progress-reporter.ts
index 16824badd095..6fc92b6c74c8 100644
--- a/packages/angular/build/src/builders/karma/progress-reporter.ts
+++ b/packages/angular/build/src/builders/karma/progress-reporter.ts
@@ -59,8 +59,18 @@ export function injectKarmaReporter(
break;
}
+ if (controller.desiredSize === null) {
+ break;
+ }
+
if (buildOutput.kind === ResultKind.Failure) {
- controller.enqueue({ success: false, message: 'Build failed' });
+ if (controller.desiredSize !== null) {
+ try {
+ controller.enqueue({ success: false, message: 'Build failed' });
+ } catch {
+ // Stream controller may already be closed or cancelled
+ }
+ }
} else if (
buildOutput.kind === ResultKind.Incremental ||
buildOutput.kind === ResultKind.Full
@@ -81,10 +91,12 @@ export function injectKarmaReporter(
}
onRunComplete = function (_browsers: unknown, results: RunCompleteInfo): void {
- if (results.exitCode === 0) {
- controller.enqueue({ success: true });
- } else {
- controller.enqueue({ success: false });
+ if (controller.desiredSize !== null) {
+ try {
+ controller.enqueue({ success: results.exitCode === 0 });
+ } catch {
+ // Stream controller may already be closed or cancelled
+ }
}
};
}
diff --git a/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts b/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts
index 366cc2aa20a1..a69698b8d743 100644
--- a/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts
+++ b/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts
@@ -28,5 +28,18 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
expect(result?.success).toBeFalse();
});
+
+ it('handles stream cancellation gracefully in watch mode', async () => {
+ harness.useTarget('test', {
+ ...BASE_OPTIONS,
+ watch: true,
+ });
+
+ const { result } = await harness.executeOnce({
+ outputLogsOnFailure: false,
+ });
+
+ expect(result?.success).toBeTrue();
+ });
});
});
diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts
index d3402b090e0f..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';
@@ -58,7 +58,7 @@ export async function normalizeOptions(
: process.execArgv.includes('--preserve-symlinks');
// Setup base paths based on workspace root and project information
- const workspaceRoot = canonicalizePath(context.workspaceRoot, preserveSymlinks);
+ const workspaceRoot = canonicalizePath(context.workspaceRoot);
const projectMetadata = await context.getProjectMetadata(projectName);
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);
@@ -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 50d57d0e5a01..1e99d84c9832 100644
--- a/packages/angular/build/src/private.ts
+++ b/packages/angular/build/src/private.ts
@@ -32,10 +32,9 @@ export {
} from './builders/dev-server/options';
// Tools
-export * from './tools/babel/plugins';
export type { ExternalResultMetadata } from './tools/esbuild/bundler-execution-result';
export { emitFilesToDisk } from './tools/esbuild/utils';
-export { transformSupportedBrowsersToTargets } from './tools/esbuild/utils';
+export { transformSupportedBrowsersToTargets } from './tools/esbuild/target';
export { SassWorkerImplementation } from './tools/sass/sass-service';
export { SourceFileCache } from './tools/esbuild/angular/source-file-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-ast-host.ts b/packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts
new file mode 100644
index 000000000000..5a11959fff66
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/linker/oxc-ast-host.ts
@@ -0,0 +1,296 @@
+/**
+ * @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 { AstHost, Range } from '@angular/compiler-cli/linker';
+import { FatalLinkerError } from '@angular/compiler-cli/linker';
+import type {
+ ArrayExpression,
+ ArrowFunctionExpression,
+ BooleanLiteral,
+ CallExpression,
+ Function as FunctionNode,
+ Node,
+ NullLiteral,
+ NumericLiteral,
+ ObjectExpression,
+ StringLiteral,
+ UnaryExpression,
+} from '@oxc-project/types';
+
+function isNode(node: unknown): node is Node {
+ return typeof node === 'object' && node !== null && 'type' in node;
+}
+
+/**
+ * An implementation of `AstHost` that queries information from `oxc-parser` AST nodes.
+ */
+export class OxcAstHost implements AstHost {
+ getSymbolName(node: unknown): string | null {
+ if (!isNode(node)) {
+ return null;
+ }
+
+ if (node.type === 'Identifier') {
+ return node.name;
+ } else if (node.type === 'MemberExpression') {
+ if (!node.computed && node.property.type === 'Identifier') {
+ return node.property.name;
+ }
+ }
+
+ return null;
+ }
+
+ isStringLiteral(node: unknown): node is StringLiteral {
+ return isNode(node) && node.type === 'Literal' && typeof node.value === 'string';
+ }
+
+ parseStringLiteral(str: unknown): string {
+ if (!this.isStringLiteral(str)) {
+ throw new FatalLinkerError(str as object, 'Unsupported syntax, expected a string literal.');
+ }
+
+ return str.value;
+ }
+
+ isNumericLiteral(node: unknown): node is NumericLiteral {
+ return isNode(node) && node.type === 'Literal' && typeof node.value === 'number';
+ }
+
+ parseNumericLiteral(num: unknown): number {
+ if (!this.isNumericLiteral(num)) {
+ throw new FatalLinkerError(num as object, 'Unsupported syntax, expected a numeric literal.');
+ }
+
+ return num.value;
+ }
+
+ isBooleanLiteral(node: unknown): node is BooleanLiteral | UnaryExpression {
+ if (!isNode(node)) {
+ return false;
+ }
+
+ return (
+ (node.type === 'Literal' && typeof node.value === 'boolean') || isMinifiedBooleanLiteral(node)
+ );
+ }
+
+ parseBooleanLiteral(bool: unknown): boolean {
+ if (isNode(bool)) {
+ if (bool.type === 'Literal' && typeof bool.value === 'boolean') {
+ return bool.value;
+ }
+ if (isMinifiedBooleanLiteral(bool)) {
+ return !bool.argument.value;
+ }
+ }
+
+ throw new FatalLinkerError(bool as object, 'Unsupported syntax, expected a boolean literal.');
+ }
+
+ isNull(node: unknown): node is NullLiteral {
+ return isNode(node) && node.type === 'Literal' && node.value === null;
+ }
+
+ isArrayLiteral(node: unknown): node is ArrayExpression {
+ return isNode(node) && node.type === 'ArrayExpression';
+ }
+
+ parseArrayLiteral(array: unknown): unknown[] {
+ if (!this.isArrayLiteral(array)) {
+ throw new FatalLinkerError(array as object, 'Unsupported syntax, expected an array literal.');
+ }
+
+ const result: unknown[] = [];
+
+ for (const element of array.elements) {
+ if (element === null) {
+ throw new FatalLinkerError(
+ array as object,
+ 'Unsupported syntax, element in array not to be empty.',
+ );
+ }
+ if (element.type === 'SpreadElement') {
+ throw new FatalLinkerError(
+ element as object,
+ 'Unsupported syntax, element in array not to use spread syntax.',
+ );
+ }
+ result.push(element);
+ }
+
+ return result;
+ }
+
+ isObjectLiteral(node: unknown): node is ObjectExpression {
+ return isNode(node) && node.type === 'ObjectExpression';
+ }
+
+ parseObjectLiteral(obj: unknown): Map {
+ if (!this.isObjectLiteral(obj)) {
+ throw new FatalLinkerError(obj as object, 'Unsupported syntax, expected an object literal.');
+ }
+
+ const result = new Map();
+
+ for (const property of obj.properties) {
+ if (property.type !== 'Property') {
+ throw new FatalLinkerError(
+ property as object,
+ 'Unsupported syntax, expected a property assignment.',
+ );
+ }
+
+ const keyNode = property.key;
+
+ let key: string;
+ if (keyNode.type === 'Identifier') {
+ key = keyNode.name;
+ } else if (this.isStringLiteral(keyNode)) {
+ key = keyNode.value;
+ } else if (this.isNumericLiteral(keyNode)) {
+ key = String(keyNode.value);
+ } else {
+ throw new FatalLinkerError(
+ keyNode as object,
+ 'Unsupported syntax, expected a property name.',
+ );
+ }
+
+ result.set(key, property.value);
+ }
+
+ return result;
+ }
+
+ isFunctionExpression(node: unknown): node is FunctionNode | ArrowFunctionExpression {
+ if (!isNode(node)) {
+ return false;
+ }
+
+ return (
+ node.type === 'FunctionDeclaration' ||
+ node.type === 'FunctionExpression' ||
+ node.type === 'ArrowFunctionExpression'
+ );
+ }
+
+ parseReturnValue(fn: unknown): unknown {
+ if (!this.isFunctionExpression(fn)) {
+ throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.');
+ }
+
+ const body = fn.body;
+ if (!body || !isNode(body)) {
+ throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function body.');
+ }
+
+ if (body.type !== 'BlockStatement') {
+ return body;
+ }
+
+ const statements = body.body;
+ if (statements.length !== 1) {
+ throw new FatalLinkerError(
+ body as object,
+ 'Unsupported syntax, expected a function body with a single return statement.',
+ );
+ }
+
+ const stmt = statements[0];
+ if (stmt.type !== 'ReturnStatement') {
+ throw new FatalLinkerError(
+ stmt as object,
+ 'Unsupported syntax, expected a function body with a single return statement.',
+ );
+ }
+
+ if (!stmt.argument) {
+ throw new FatalLinkerError(
+ stmt as object,
+ 'Unsupported syntax, expected function to return a value.',
+ );
+ }
+
+ return stmt.argument;
+ }
+
+ parseParameters(fn: unknown): unknown[] {
+ if (!this.isFunctionExpression(fn)) {
+ throw new FatalLinkerError(fn as object, 'Unsupported syntax, expected a function.');
+ }
+
+ return fn.params;
+ }
+
+ isCallExpression(node: unknown): node is CallExpression {
+ return isNode(node) && node.type === 'CallExpression';
+ }
+
+ parseCallee(call: unknown): unknown {
+ if (!this.isCallExpression(call)) {
+ throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.');
+ }
+
+ return call.callee;
+ }
+
+ parseArguments(call: unknown): unknown[] {
+ if (!this.isCallExpression(call)) {
+ throw new FatalLinkerError(call as object, 'Unsupported syntax, expected a call expression.');
+ }
+
+ const result: unknown[] = [];
+
+ for (const arg of call.arguments) {
+ if (arg.type === 'SpreadElement') {
+ throw new FatalLinkerError(
+ arg as object,
+ 'Unsupported syntax, argument not to use spread syntax.',
+ );
+ }
+ result.push(arg);
+ }
+
+ return result;
+ }
+
+ getRange(node: unknown): Range {
+ if (!isNode(node) || typeof node.start !== 'number' || typeof node.end !== 'number') {
+ throw new FatalLinkerError(
+ node as object,
+ 'Unable to read range for node - it is missing location information.',
+ );
+ }
+
+ return {
+ startPos: node.start,
+ startLine: 0,
+ startCol: 0,
+ endPos: node.end,
+ };
+ }
+}
+
+function isMinifiedBooleanLiteral(
+ node: Node,
+): node is UnaryExpression & { argument: NumericLiteral } {
+ if (node.type !== 'UnaryExpression') {
+ return false;
+ }
+
+ const arg = node.argument;
+
+ return (
+ node.prefix === true &&
+ node.operator === '!' &&
+ arg.type === 'Literal' &&
+ typeof arg.value === 'number' &&
+ (arg.value === 0 || arg.value === 1)
+ );
+}
diff --git a/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts b/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts
new file mode 100644
index 000000000000..79027584066d
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts
@@ -0,0 +1,195 @@
+/**
+ * @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 { FatalLinkerError } from '@angular/compiler-cli/linker';
+import { parseSync } from 'oxc-parser';
+import { OxcAstHost } from './oxc-ast-host';
+
+describe('OxcAstHost', () => {
+ let host: OxcAstHost;
+
+ beforeEach(() => {
+ host = new OxcAstHost();
+ });
+
+ function parseExpression(code: string): unknown {
+ const { program } = parseSync('test.js', `const x = ${code};`, { range: true });
+ const stmt = program.body[0] as { declarations: Array<{ init: unknown }> };
+
+ return stmt.declarations[0].init;
+ }
+
+ function parseStatement(code: string): unknown {
+ const { program } = parseSync('test.js', code, { range: true });
+
+ return program.body[0];
+ }
+
+ describe('getSymbolName', () => {
+ it('should return the name of an identifier', () => {
+ const expr = parseExpression('foo');
+ expect(host.getSymbolName(expr)).toBe('foo');
+ });
+
+ it('should return the property name of a member expression', () => {
+ const expr = parseExpression('foo.bar');
+ expect(host.getSymbolName(expr)).toBe('bar');
+ });
+
+ it('should return null for non-identifier or computed member expressions', () => {
+ expect(host.getSymbolName(parseExpression('foo[0]'))).toBeNull();
+ expect(host.getSymbolName(parseExpression('42'))).toBeNull();
+ expect(host.getSymbolName(null)).toBeNull();
+ });
+ });
+
+ describe('string literals', () => {
+ it('should recognize and parse valid string literals', () => {
+ const expr = parseExpression('"hello"');
+ expect(host.isStringLiteral(expr)).toBe(true);
+ expect(host.parseStringLiteral(expr)).toBe('hello');
+ });
+
+ it('should throw when parsing non-string literals', () => {
+ const expr = parseExpression('123');
+ expect(host.isStringLiteral(expr)).toBe(false);
+ expect(() => host.parseStringLiteral(expr)).toThrowError(FatalLinkerError);
+ });
+ });
+
+ describe('numeric literals', () => {
+ it('should recognize and parse valid numeric literals', () => {
+ const expr = parseExpression('123');
+ expect(host.isNumericLiteral(expr)).toBe(true);
+ expect(host.parseNumericLiteral(expr)).toBe(123);
+ });
+
+ it('should throw when parsing non-numeric literals', () => {
+ const expr = parseExpression('"123"');
+ expect(host.isNumericLiteral(expr)).toBe(false);
+ expect(() => host.parseNumericLiteral(expr)).toThrowError(FatalLinkerError);
+ });
+ });
+
+ describe('boolean literals', () => {
+ it('should recognize and parse true and false literals', () => {
+ const trueExpr = parseExpression('true');
+ const falseExpr = parseExpression('false');
+ expect(host.isBooleanLiteral(trueExpr)).toBe(true);
+ expect(host.parseBooleanLiteral(trueExpr)).toBe(true);
+ expect(host.isBooleanLiteral(falseExpr)).toBe(true);
+ expect(host.parseBooleanLiteral(falseExpr)).toBe(false);
+ });
+
+ it('should recognize and parse minified boolean literals (!0 and !1)', () => {
+ const trueExpr = parseExpression('!0');
+ const falseExpr = parseExpression('!1');
+ expect(host.isBooleanLiteral(trueExpr)).toBe(true);
+ expect(host.parseBooleanLiteral(trueExpr)).toBe(true);
+ expect(host.isBooleanLiteral(falseExpr)).toBe(true);
+ expect(host.parseBooleanLiteral(falseExpr)).toBe(false);
+ });
+
+ it('should return false for invalid boolean expressions', () => {
+ expect(host.isBooleanLiteral(parseExpression('!2'))).toBe(false);
+ expect(() => host.parseBooleanLiteral(parseExpression('!2'))).toThrowError(FatalLinkerError);
+ });
+ });
+
+ describe('array literals', () => {
+ it('should recognize and parse array literals', () => {
+ const expr = parseExpression('[1, "a", true]');
+ expect(host.isArrayLiteral(expr)).toBe(true);
+ expect(host.parseArrayLiteral(expr).length).toBe(3);
+ });
+
+ it('should throw when array contains empty elements or spread syntax', () => {
+ expect(() => host.parseArrayLiteral(parseExpression('[1, , 2]'))).toThrowError(
+ FatalLinkerError,
+ );
+ expect(() => host.parseArrayLiteral(parseExpression('[1, ...a]'))).toThrowError(
+ FatalLinkerError,
+ );
+ });
+ });
+
+ describe('object literals', () => {
+ it('should recognize and parse object literals into a Map', () => {
+ const expr = parseExpression('{ a: 1, "b": 2, 3: "c" }');
+ expect(host.isObjectLiteral(expr)).toBe(true);
+
+ const map = host.parseObjectLiteral(expr);
+ expect(map.size).toBe(3);
+ expect(map.has('a')).toBe(true);
+ expect(map.has('b')).toBe(true);
+ expect(map.has('3')).toBe(true);
+ });
+
+ it('should throw when object literal contains spread or non-property assignments', () => {
+ expect(() => host.parseObjectLiteral(parseExpression('{ ...a }'))).toThrowError(
+ FatalLinkerError,
+ );
+ });
+ });
+
+ describe('functions', () => {
+ it('should parse return value and parameters from arrow functions', () => {
+ const expr = parseExpression('(a, b) => 42');
+ expect(host.isFunctionExpression(expr)).toBe(true);
+ expect(host.parseParameters(expr).length).toBe(2);
+
+ const returnValue = host.parseReturnValue(expr);
+ expect(host.isNumericLiteral(returnValue)).toBe(true);
+ });
+
+ it('should parse return value from function with block statement containing single return', () => {
+ const stmt = parseStatement('function foo(a) { return "hello"; }');
+ expect(host.isFunctionExpression(stmt)).toBe(true);
+
+ const returnValue = host.parseReturnValue(stmt);
+ expect(host.isStringLiteral(returnValue)).toBe(true);
+ });
+
+ it('should throw when function body has multiple statements or no return', () => {
+ expect(() =>
+ host.parseReturnValue(parseStatement('function foo() { const x = 1; return x; }')),
+ ).toThrowError(FatalLinkerError);
+ expect(() => host.parseReturnValue(parseStatement('function foo() {}'))).toThrowError(
+ FatalLinkerError,
+ );
+ });
+ });
+
+ describe('call expressions', () => {
+ it('should parse callee and arguments', () => {
+ const expr = parseExpression('foo(1, "a")');
+ expect(host.isCallExpression(expr)).toBe(true);
+ expect(host.getSymbolName(host.parseCallee(expr))).toBe('foo');
+ expect(host.parseArguments(expr).length).toBe(2);
+ });
+
+ it('should throw when call expression arguments contain spread syntax', () => {
+ expect(() => host.parseArguments(parseExpression('foo(...args)'))).toThrowError(
+ FatalLinkerError,
+ );
+ });
+ });
+
+ describe('getRange', () => {
+ it('should return range object for valid AST nodes', () => {
+ const expr = parseExpression('foo');
+ const range = host.getRange(expr);
+ expect(range.startPos).toBeGreaterThanOrEqual(0);
+ expect(range.endPos).toBeGreaterThan(range.startPos);
+ });
+
+ it('should throw when node is missing range offsets', () => {
+ expect(() => host.getRange({})).toThrowError(FatalLinkerError);
+ });
+ });
+});
diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts
new file mode 100644
index 000000000000..26133a6b5611
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts
@@ -0,0 +1,96 @@
+/**
+ * @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 { ConsoleLogger, LogLevel } from '@angular/compiler-cli';
+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 } from '@oxc-project/types';
+import { OxcAstHost } from './oxc-ast-host';
+import { StringAstFactory } from './string-ast-factory';
+
+/**
+ * A declaration scope that instructs the Angular compiler to emit constant pools
+ * inside a local IIFE around each linked declaration rather than hoisting shared
+ * constants to the module level.
+ *
+ * Preferred due to:
+ * - In-Place String Replacement: Enables fast in-place string replacements in
+ * `MagicString` without parsing or mutating surrounding ES module statements.
+ * - Better Tree-Shaking Locality: Component constants are strictly encapsulated
+ * within the component's `@__PURE__` IIFE closure (`(function() { ... })()`). If a
+ * bundler tree-shakes an unused component from a library FESM, all of its associated
+ * constants are automatically eliminated without leaving orphan top-level variables.
+ * - Negligible Wire Size Impact: LZ77/Brotli compression deduplicates repeated IIFE
+ * wrappers and array literals over the wire to near-zero marginal cost.
+ */
+class InlineDeclarationScope implements DeclarationScope {
+ getConstantScopeRef(): null {
+ return null;
+ }
+}
+
+const noopFileSystem: ReadonlyFileSystem = {
+ exists: () => false,
+ readFile: () => '',
+ resolve: (...paths: string[]) => paths.join('/'),
+ dirname: (path: string) => path.split('/').slice(0, -1).join('/'),
+ relative: (_from: string, to: string) => to,
+} as unknown as ReadonlyFileSystem;
+
+let SHARED_LOGGER: ConsoleLogger;
+let SHARED_AST_HOST: OxcAstHost;
+let SHARED_DECLARATION_SCOPE: InlineDeclarationScope;
+
+/**
+ * Manages Angular partial declaration linking using Oxc AST nodes.
+ */
+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);
+ }
+
+ /**
+ * 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 || !this.#fileLinker.isPartialDeclaration(calleeName)) {
+ return undefined;
+ }
+
+ const args = SHARED_AST_HOST.parseArguments(node);
+ const linkedCode = this.#fileLinker.linkPartialDeclaration(
+ calleeName,
+ args,
+ SHARED_DECLARATION_SCOPE,
+ );
+
+ return linkedCode as string;
+ }
+}
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
new file mode 100644
index 000000000000..fa62c1c5ece1
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @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 { transform } from '../../oxc/oxc-transform';
+
+describe('oxc-linker', () => {
+ it('should not modify code that does not need linking', () => {
+ const input = 'const x = 1;';
+ const result = transform('test.js', input, { link: true, advancedOptimizations: false });
+ expect(result.code).toBe(input);
+ expect(result.map).toBeUndefined();
+ });
+
+ it('should link a partial directive declaration', () => {
+ 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 });
+ expect(result.code).toContain('i0.ɵɵdefineDirective');
+ expect(result.code).not.toContain('i0.ɵɵngDeclareDirective');
+ });
+
+ it('should link a partial component declaration', () => {
+ const input = `
+ import * as i0 from "@angular/core";
+ export class MyComponent {}
+ MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({
+ minVersion: "12.0.0",
+ version: "14.0.0",
+ ngImport: i0,
+ type: MyComponent,
+ isStandalone: true,
+ selector: "my-cmp",
+ template: "Hello"
+ });
+ `;
+
+ 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/angular/linker/string-ast-factory.ts b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts
new file mode 100644
index 000000000000..a703856c39bd
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts
@@ -0,0 +1,335 @@
+/**
+ * @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 {
+ AstFactory,
+ BinaryOperator,
+ LeadingComment,
+ ObjectLiteralProperty,
+ SourceMapRange,
+ TemplateLiteral,
+ UnaryOperator,
+ VariableDeclarationType,
+} from '@angular/compiler-cli/src/ngtsc/translator';
+import type {
+ BuiltInType,
+ Parameter,
+} from '@angular/compiler-cli/src/ngtsc/translator/src/api/ast_factory';
+
+const OBJECT_LITERAL_PREFIX = '\0obj_';
+
+/**
+ * An implementation of `AstFactory` that generates JavaScript code strings directly.
+ */
+export class StringAstFactory implements AstFactory {
+ constructor(private readonly sourceCode: string = '') {}
+
+ private render(expr: unknown): string {
+ let rendered: string;
+ if (typeof expr === 'string') {
+ rendered = expr;
+ } else if (
+ typeof expr === 'object' &&
+ expr !== null &&
+ typeof (expr as { start?: number; end?: number }).start === 'number' &&
+ typeof (expr as { end?: number }).end === 'number'
+ ) {
+ const { start, end } = expr as { start: number; end: number };
+
+ rendered = this.sourceCode.slice(start, end);
+ } else {
+ rendered = String(expr);
+ }
+
+ if (rendered.startsWith(OBJECT_LITERAL_PREFIX)) {
+ return rendered.slice(OBJECT_LITERAL_PREFIX.length);
+ }
+
+ return rendered;
+ }
+
+ /**
+ * Wraps an expression in parentheses if it has syntax or operator precedence
+ * traps when used as the receiver/callee of property access, element access,
+ * call chains, `new` expressions, or tagged templates.
+ *
+ * For example, arrow functions (`(() => {}).foo`), numbers (`(5).foo`), object
+ * literals (`({ a: 1 }).foo`), and unary operators (`(void 0)()`) require
+ * parenthesization when used as receivers.
+ */
+ private wrapReceiver(receiver: unknown): string {
+ let rendered = this.render(receiver);
+ if (
+ rendered.startsWith('function') ||
+ rendered.startsWith('{') ||
+ rendered.includes('=>') ||
+ /^(?:typeof|void|delete|new|await|throw|return)\b/.test(rendered) ||
+ /^[!~+-]/.test(rendered) ||
+ /^\d/.test(rendered)
+ ) {
+ rendered = `(${rendered})`;
+ }
+
+ return rendered;
+ }
+
+ /**
+ * Wraps an expression in parentheses if it is an un-parenthesized complex
+ * operand (such as an un-parenthesized binary, logical, or assignment expression)
+ * to prevent operator precedence inversion when embedded inside an outer expression.
+ *
+ * Atomic expressions (identifiers, member/element access chains, and simple
+ * literals matching `/^[a-zA-Z0-9_$.[\]"'`]+$/`) and already-parenthesized
+ * expressions are returned as-is.
+ */
+ private wrapOperand(expr: unknown): string {
+ const rendered = this.render(expr);
+ if (
+ (rendered.startsWith('(') && rendered.endsWith(')')) ||
+ /^[a-zA-Z0-9_$.[\]"'`]+$/.test(rendered)
+ ) {
+ return rendered;
+ }
+
+ return `(${rendered})`;
+ }
+
+ /**
+ * Attaching statement-level comments is a no-op in `StringAstFactory`.
+ *
+ * Why this is safe:
+ * - Load-bearing tree-shaking annotations (`@__PURE__` markers) are explicitly prepended by
+ * `createCallExpression` and `createCallChain` whenever `pure: true`.
+ * - In `@angular/compiler`, `LeadingComment` is only used for `@ts-ignore` (suppressing
+ * TypeScript checker errors in generated `.ts` code) and `webpackChunkName` (in dynamic
+ * imports), neither of which occur during `.js`/`.mjs` library partial linking.
+ */
+ attachComments(_statement: string, _leadingComments: LeadingComment[]): void {}
+
+ createArrayLiteral(elements: unknown[]): string {
+ return `[${elements.map((e) => this.render(e)).join(', ')}]`;
+ }
+
+ createAssignment(target: unknown, operator: BinaryOperator, value: unknown): string {
+ return `(${this.render(target)} ${operator} ${this.render(value)})`;
+ }
+
+ createBinaryExpression(
+ leftOperand: unknown,
+ operator: BinaryOperator,
+ rightOperand: unknown,
+ ): string {
+ return `(${this.wrapOperand(leftOperand)} ${operator} ${this.wrapOperand(rightOperand)})`;
+ }
+
+ createBlock(body: string[]): string {
+ return `{\n${body.join('\n')}\n}`;
+ }
+
+ createCallExpression(callee: unknown, args: unknown[], pure: boolean): string {
+ const annotation = pure ? '/*@__PURE__*/ ' : '';
+
+ return `${annotation}${this.wrapReceiver(callee)}(${args.map((a) => this.render(a)).join(', ')})`;
+ }
+
+ createCallChain(callee: unknown, args: unknown[], pure: boolean, isOptional: boolean): string {
+ const annotation = pure ? '/*@__PURE__*/ ' : '';
+ const operator = isOptional ? '?.' : '';
+
+ return `${annotation}${this.wrapReceiver(callee)}${operator}(${args.map((a) => this.render(a)).join(', ')})`;
+ }
+
+ createConditional(condition: unknown, thenExpression: unknown, elseExpression: unknown): string {
+ return `(${this.render(condition)} ? ${this.render(thenExpression)} : ${this.render(elseExpression)})`;
+ }
+
+ createElementAccess(expression: unknown, element: unknown): string {
+ return `${this.wrapReceiver(expression)}[${this.render(element)}]`;
+ }
+
+ createElementAccessChain(expression: unknown, element: unknown, isOptional: boolean): string {
+ const operator = isOptional ? '?.' : '';
+
+ return `${this.wrapReceiver(expression)}${operator}[${this.render(element)}]`;
+ }
+
+ createExpressionStatement(expression: unknown): string {
+ return `${this.render(expression)};`;
+ }
+
+ createFunctionDeclaration(
+ functionName: string,
+ parameters: Parameter[],
+ body: string,
+ ): string {
+ const params = parameters.map((p) => p.name).join(', ');
+
+ return `function ${functionName}(${params}) ${body}`;
+ }
+
+ createFunctionExpression(
+ functionName: string | null,
+ parameters: Parameter[],
+ body: string,
+ ): string {
+ const name = functionName ? ` ${functionName}` : '';
+ const params = parameters.map((p) => p.name).join(', ');
+
+ return `function${name}(${params}) ${body}`;
+ }
+
+ createArrowFunctionExpression(parameters: Parameter[], body: unknown): string {
+ const params = parameters.map((p) => p.name).join(', ');
+ const isObjectLiteral = typeof body === 'string' && body.startsWith(OBJECT_LITERAL_PREFIX);
+ const renderedBody = this.render(body);
+ const formattedBody = isObjectLiteral ? `(${renderedBody})` : renderedBody;
+
+ return `(${params}) => ${formattedBody}`;
+ }
+
+ createDynamicImport(url: unknown): string {
+ return `import(${this.render(url)})`;
+ }
+
+ createIdentifier(name: string): string {
+ return name;
+ }
+
+ createIfStatement(
+ condition: unknown,
+ thenStatement: string,
+ elseStatement: string | null,
+ ): string {
+ const elseClause = elseStatement ? ` else ${elseStatement}` : '';
+
+ return `if (${this.render(condition)}) ${thenStatement}${elseClause}`;
+ }
+
+ createLiteral(value: string | number | boolean | null | undefined): string {
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
+ }
+
+ createNewExpression(expression: unknown, args: unknown[]): string {
+ return `new ${this.wrapReceiver(expression)}(${args.map((a) => this.render(a)).join(', ')})`;
+ }
+
+ createObjectLiteral(properties: ObjectLiteralProperty[]): string {
+ const props = properties.map((p) => {
+ if (p.kind === 'spread') {
+ return `...${this.render(p.expression)}`;
+ }
+
+ const key = p.quoted ? JSON.stringify(p.propertyName) : p.propertyName;
+
+ return `${key}: ${this.render(p.value)}`;
+ });
+
+ return `${OBJECT_LITERAL_PREFIX}{\n${props.join(',\n')}\n}`;
+ }
+
+ createParenthesizedExpression(expression: unknown): string {
+ return `(${this.render(expression)})`;
+ }
+
+ createPropertyAccess(expression: unknown, propertyName: string): string {
+ return `${this.wrapReceiver(expression)}.${propertyName}`;
+ }
+
+ createPropertyAccessChain(
+ expression: unknown,
+ propertyName: string,
+ isOptional: boolean,
+ ): string {
+ const operator = isOptional ? '?.' : '.';
+
+ return `${this.wrapReceiver(expression)}${operator}${propertyName}`;
+ }
+
+ createReturnStatement(expression: unknown | null): string {
+ return `return${expression !== null ? ` ${this.render(expression)}` : ''};`;
+ }
+
+ createTaggedTemplate(tag: unknown, template: TemplateLiteral): string {
+ return `${this.wrapReceiver(tag)}${this.createTemplateLiteral(template)}`;
+ }
+
+ createTemplateLiteral(template: TemplateLiteral): string {
+ let result = '`';
+ for (let i = 0; i < template.elements.length; i++) {
+ result += template.elements[i].raw;
+ if (i < template.expressions.length) {
+ result += `\${${this.render(template.expressions[i])}}`;
+ }
+ }
+ result += '`';
+
+ return result;
+ }
+
+ createThrowStatement(expression: unknown): string {
+ return `throw ${this.render(expression)};`;
+ }
+
+ createTypeOfExpression(expression: unknown): string {
+ return `typeof ${this.wrapOperand(expression)}`;
+ }
+
+ createVoidExpression(expression: unknown): string {
+ return `void ${this.wrapOperand(expression)}`;
+ }
+
+ createUnaryExpression(operator: UnaryOperator, operand: unknown): string {
+ return `${operator}${this.wrapOperand(operand)}`;
+ }
+
+ createVariableDeclaration(
+ variableName: string,
+ initializer: unknown | null,
+ variableType: VariableDeclarationType,
+ _type: string | null = null,
+ ): string {
+ const init = initializer !== null ? ` = ${this.render(initializer)}` : '';
+
+ return `${variableType} ${variableName}${init};`;
+ }
+
+ createRegularExpressionLiteral(body: string, flags: string | null): string {
+ return `/${body}/${flags ?? ''}`;
+ }
+
+ createSpreadElement(expression: unknown): string {
+ return `...${this.render(expression)}`;
+ }
+
+ createBuiltInType(_type: BuiltInType): string {
+ return '';
+ }
+
+ createExpressionType(_expression: unknown, _typeParams: string[] | null): string {
+ return '';
+ }
+
+ createArrayType(_elementType: string): string {
+ return '';
+ }
+
+ createMapType(_valueType: string): string {
+ return '';
+ }
+
+ transplantType(_type: string): string {
+ return '';
+ }
+
+ setSourceMapRange(
+ node: T,
+ _sourceMapRange: SourceMapRange | null,
+ ): T {
+ return node;
+ }
+}
diff --git a/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts b/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts
new file mode 100644
index 000000000000..eb62a358dfcd
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts
@@ -0,0 +1,210 @@
+/**
+ * @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 { StringAstFactory } from './string-ast-factory';
+
+describe('StringAstFactory', () => {
+ let factory: StringAstFactory;
+
+ beforeEach(() => {
+ factory = new StringAstFactory('const foo = bar;');
+ });
+
+ describe('source slicing (render)', () => {
+ it('should slice original source code when given an AST object with start and end offsets', () => {
+ const astNode = { start: 6, end: 9 };
+ expect(factory.createExpressionStatement(astNode)).toBe('foo;');
+ });
+ });
+
+ describe('literals and identifiers', () => {
+ it('should correctly format string literals', () => {
+ expect(factory.createLiteral('hello')).toBe('"hello"');
+ });
+
+ it('should correctly format numeric literals including NaN and Infinity', () => {
+ expect(factory.createLiteral(123)).toBe('123');
+ expect(factory.createLiteral(NaN)).toBe('NaN');
+ expect(factory.createLiteral(Infinity)).toBe('Infinity');
+ });
+
+ it('should correctly format boolean literals', () => {
+ expect(factory.createLiteral(true)).toBe('true');
+ expect(factory.createLiteral(false)).toBe('false');
+ });
+
+ it('should correctly format null and undefined literals', () => {
+ expect(factory.createLiteral(null)).toBe('null');
+ expect(factory.createLiteral(undefined)).toBe('undefined');
+ });
+
+ it('should correctly create identifiers', () => {
+ expect(factory.createIdentifier('myVar')).toBe('myVar');
+ });
+
+ it('should correctly format regular expression literals', () => {
+ expect(factory.createRegularExpressionLiteral('abc+', 'g')).toBe('/abc+/g');
+ expect(factory.createRegularExpressionLiteral('abc+', null)).toBe('/abc+/');
+ });
+ });
+
+ describe('array and object literals', () => {
+ it('should correctly format array literals', () => {
+ expect(factory.createArrayLiteral(['1', '2', '3'])).toBe('[1, 2, 3]');
+ });
+
+ it('should correctly format object literals with quoted and unquoted keys', () => {
+ const props = [
+ { kind: 'property' as const, propertyName: 'foo', value: '1', quoted: false },
+ { kind: 'property' as const, propertyName: 'foo-bar', value: '2', quoted: true },
+ ];
+ expect(factory.createObjectLiteral(props)).toContain('{\nfoo: 1,\n"foo-bar": 2\n}');
+ });
+
+ it('should correctly format object literals with spread properties', () => {
+ const props = [
+ { kind: 'property' as const, propertyName: 'foo', value: '1', quoted: false },
+ { kind: 'spread' as const, expression: 'bar' },
+ ];
+ expect(factory.createObjectLiteral(props)).toContain('{\nfoo: 1,\n...bar\n}');
+ });
+ });
+
+ describe('property and element access', () => {
+ it('should correctly format property access and property access chains', () => {
+ expect(factory.createPropertyAccess('foo', 'bar')).toBe('foo.bar');
+ expect(factory.createPropertyAccessChain('foo', 'bar', true)).toBe('foo?.bar');
+ expect(factory.createPropertyAccessChain('foo', 'bar', false)).toBe('foo.bar');
+ });
+
+ it('should correctly format element access and element access chains', () => {
+ expect(factory.createElementAccess('foo', '"bar"')).toBe('foo["bar"]');
+ expect(factory.createElementAccessChain('foo', '"bar"', true)).toBe('foo?.["bar"]');
+ expect(factory.createElementAccessChain('foo', '"bar"', false)).toBe('foo["bar"]');
+ });
+
+ it('should parenthesize precedence-sensitive receivers in property and element access', () => {
+ expect(factory.createPropertyAccess('() => {}', 'bar')).toBe('(() => {}).bar');
+ expect(factory.createPropertyAccess('5', 'bar')).toBe('(5).bar');
+ expect(factory.createPropertyAccess('typeof x', 'bar')).toBe('(typeof x).bar');
+ expect(factory.createElementAccess('() => {}', '"bar"')).toBe('(() => {})["bar"]');
+ });
+ });
+
+ describe('call expressions and chains', () => {
+ it('should format standard and pure call expressions', () => {
+ expect(factory.createCallExpression('foo', ['1', '2'], false)).toBe('foo(1, 2)');
+ expect(factory.createCallExpression('foo', ['1', '2'], true)).toBe('/*@__PURE__*/ foo(1, 2)');
+ });
+
+ it('should wrap arrow functions and function expressions in parentheses when used as callees', () => {
+ expect(factory.createCallExpression('() => {}', [], false)).toBe('(() => {})()');
+ expect(factory.createCallExpression('function() {}', [], false)).toBe('(function() {})()');
+ expect(factory.createCallChain('() => {}', [], false, true)).toBe('(() => {})?.()');
+ expect(factory.createNewExpression('() => Foo', [])).toBe('new (() => Foo)()');
+ });
+
+ it('should format optional call chains', () => {
+ expect(factory.createCallChain('foo', ['1'], false, true)).toBe('foo?.(1)');
+ expect(factory.createCallChain('foo', ['1'], false, false)).toBe('foo(1)');
+ });
+
+ it('should format new expressions', () => {
+ expect(factory.createNewExpression('Foo', ['1', '2'])).toBe('new Foo(1, 2)');
+ });
+ });
+
+ describe('operators and expressions', () => {
+ it('should format binary and assignment expressions with parentheses', () => {
+ expect(factory.createBinaryExpression('a', '+', 'b')).toBe('(a + b)');
+ expect(factory.createBinaryExpression('a + b', '*', 'c + d')).toBe('((a + b) * (c + d))');
+ expect(factory.createAssignment('a', '=', 'b')).toBe('(a = b)');
+ });
+
+ it('should format conditional expressions with parentheses', () => {
+ expect(factory.createConditional('cond', 'trueVal', 'falseVal')).toBe(
+ '(cond ? trueVal : falseVal)',
+ );
+ });
+
+ it('should format unary expressions', () => {
+ expect(factory.createUnaryExpression('!', 'foo')).toBe('!foo');
+ expect(factory.createTypeOfExpression('foo')).toBe('typeof foo');
+ expect(factory.createVoidExpression('foo')).toBe('void foo');
+ expect(factory.createUnaryExpression('!', 'a + b')).toBe('!(a + b)');
+ });
+ });
+
+ describe('functions and statements', () => {
+ it('should format function declarations and expressions', () => {
+ const params = [
+ { name: 'a', type: null },
+ { name: 'b', type: null },
+ ];
+ expect(factory.createFunctionDeclaration('foo', params, '{ return a + b; }')).toBe(
+ 'function foo(a, b) { return a + b; }',
+ );
+ expect(factory.createFunctionExpression('foo', params, '{ return a + b; }')).toBe(
+ 'function foo(a, b) { return a + b; }',
+ );
+ expect(factory.createFunctionExpression(null, params, '{ return a + b; }')).toBe(
+ 'function(a, b) { return a + b; }',
+ );
+ });
+
+ it('should format arrow functions', () => {
+ const params = [
+ { name: 'a', type: null },
+ { name: 'b', type: null },
+ ];
+ expect(factory.createArrowFunctionExpression(params, 'a + b')).toBe('(a, b) => a + b');
+ });
+
+ it('should parenthesize object literal expression bodies in arrow functions even when containing semicolons', () => {
+ const params = [{ name: 'a', type: null }];
+ const obj = factory.createObjectLiteral([
+ {
+ kind: 'property',
+ propertyName: 'factory',
+ value: '() => { return new Service(); }',
+ quoted: false,
+ },
+ ]);
+ expect(factory.createArrowFunctionExpression(params, obj)).toBe(
+ '(a) => ({\nfactory: () => { return new Service(); }\n})',
+ );
+ });
+
+ it('should format statements', () => {
+ expect(factory.createBlock(['foo();', 'bar();'])).toBe('{\nfoo();\nbar();\n}');
+ expect(factory.createIfStatement('cond', 'foo();', 'bar();')).toBe(
+ 'if (cond) foo(); else bar();',
+ );
+ expect(factory.createIfStatement('cond', 'foo();', null)).toBe('if (cond) foo();');
+ expect(factory.createReturnStatement('val')).toBe('return val;');
+ expect(factory.createReturnStatement(null)).toBe('return;');
+ expect(factory.createThrowStatement('err')).toBe('throw err;');
+ expect(factory.createVariableDeclaration('x', '1', 'const')).toBe('const x = 1;');
+ expect(factory.createVariableDeclaration('y', null, 'let')).toBe('let y;');
+ });
+ });
+
+ describe('template literals', () => {
+ it('should format template literals and tagged templates', () => {
+ const template = {
+ elements: [
+ { raw: 'Hello ', cooked: 'Hello ', range: null },
+ { raw: '!', cooked: '!', range: null },
+ ],
+ expressions: ['name'],
+ };
+ expect(factory.createTemplateLiteral(template)).toBe('`Hello ${name}!`');
+ expect(factory.createTaggedTemplate('tag', template)).toBe('tag`Hello ${name}!`');
+ });
+ });
+});
diff --git a/packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts b/packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts
deleted file mode 100644
index e549db97f8bb..000000000000
--- a/packages/angular/build/src/tools/babel/plugins/add-code-coverage.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * @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 { NodePath, PluginObject, PluginPass, types } from '@babel/core';
-import type { Visitor } from 'istanbul-lib-instrument';
-import assert from 'node:assert';
-
-/**
- * A babel plugin factory function for adding istanbul instrumentation.
- *
- * @returns A babel plugin object instance.
- */
-export default function (
- programVisitor: typeof import('istanbul-lib-instrument').programVisitor,
-): PluginObject {
- const visitors = new WeakMap();
-
- return {
- visitor: {
- Program: {
- enter(path: NodePath, state: PluginPass) {
- const visitor = programVisitor(types, state.filename, {
- // Babel returns a Converter object from the `convert-source-map` package
- inputSourceMap: (state.file.inputMap as undefined | { toObject(): object })?.toObject(),
- });
- visitors.set(path, visitor);
-
- visitor.enter(path);
- },
- exit(path: NodePath) {
- const visitor = visitors.get(path);
- assert(visitor, 'Instrumentation visitor should always be present for program path.');
-
- visitor.exit(path);
- visitors.delete(path);
- },
- },
- },
- };
-}
diff --git a/packages/angular/build/src/tools/babel/plugins/index.ts b/packages/angular/build/src/tools/babel/plugins/index.ts
deleted file mode 100644
index dfdcd0b2d6b9..000000000000
--- a/packages/angular/build/src/tools/babel/plugins/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @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
- */
-
-export { default as adjustStaticMembers } from './adjust-static-class-members';
-export { default as adjustTypeScriptEnums } from './adjust-typescript-enums';
-export { default as elideAngularMetadata } from './elide-angular-metadata';
-export { default as markTopLevelPure } from './pure-toplevel-functions';
diff --git a/packages/angular/build/src/tools/babel/plugins/types.d.ts b/packages/angular/build/src/tools/babel/plugins/types.d.ts
deleted file mode 100644
index 4ff052dcb136..000000000000
--- a/packages/angular/build/src/tools/babel/plugins/types.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * @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
- */
-
-declare module 'istanbul-lib-instrument' {
- export interface Visitor {
- enter(path: import('@babel/core').NodePath): void;
- exit(path: import('@babel/core').NodePath): void;
- }
-
- export function programVisitor(
- types: typeof import('@babel/core').types,
- filePath?: string,
- options?: { inputSourceMap?: object | null },
- ): Visitor;
-}
diff --git a/packages/angular/build/src/tools/babel/typings.d.ts b/packages/angular/build/src/tools/babel/typings.d.ts
deleted file mode 100644
index c2132a921dd9..000000000000
--- a/packages/angular/build/src/tools/babel/typings.d.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * @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
- */
-
-declare module '@babel/helper-annotate-as-pure' {
- export default function annotateAsPure(
- pathOrNode: import('@babel/types').Node | { node: import('@babel/types').Node },
- ): void;
-}
-
-declare module '@babel/helper-split-export-declaration' {
- export default function splitExportDeclaration(
- exportDeclaration: import('@babel/core').NodePath<
- import('@babel/types').ExportDefaultDeclaration
- >,
- ): void;
-}
diff --git a/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts b/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts
index 79fb9715bdbd..286411a06057 100644
--- a/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts
@@ -46,13 +46,14 @@ export class AngularCompilationContext {
this.#pendingCompilation = true;
}
- #disposed = false;
+ #disposal: Promise | undefined;
- async dispose(): Promise {
- if (this.#disposed) {
- return;
- }
- this.#disposed = true;
+ dispose(): Promise {
+ // Reuse any in progress disposal to ensure all callers can await completion
+ return (this.#disposal ??= this.#close());
+ }
+
+ async #close(): Promise {
this.markAsReady(true);
try {
await this.#compilation.close?.();
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/rewrite-bazel-paths.ts b/packages/angular/build/src/tools/esbuild/angular/rewrite-bazel-paths.ts
index 8a6fb6aa82a4..b7cd376d560e 100644
--- a/packages/angular/build/src/tools/esbuild/angular/rewrite-bazel-paths.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/rewrite-bazel-paths.ts
@@ -6,25 +6,55 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import { join, relative } from 'node:path';
+import { join, relative, sep } from 'node:path';
const bazelBinDirectory = process.env['BAZEL_BINDIR'];
const bazelExecRoot = process.env['JS_BINARY__EXECROOT'];
+const execRootMarker = `${sep}execroot${sep}`;
+
+/**
+ * Sandboxed actions run in a copy of the execroot whose absolute path differs from
+ * `JS_BINARY__EXECROOT`, so the effective execroot is derived from the working directory.
+ */
+function effectiveExecRoot(): string | undefined {
+ const cwd = process.cwd();
+ const markerIndex = cwd.indexOf(execRootMarker);
+ if (markerIndex === -1) {
+ return bazelExecRoot;
+ }
+
+ const workspaceEnd = cwd.indexOf(sep, markerIndex + execRootMarker.length);
+
+ return workspaceEnd === -1 ? cwd : cwd.slice(0, workspaceEnd);
+}
export function rewriteForBazel(path: string): string {
if (!bazelBinDirectory || !bazelExecRoot) {
return path;
}
- const fromExecRoot = relative(bazelExecRoot, path);
+ const execRoot = effectiveExecRoot() ?? bazelExecRoot;
+
+ const fromExecRoot = relative(execRoot, path);
if (!fromExecRoot.startsWith('..')) {
return path;
}
const fromBinDirectory = relative(bazelBinDirectory, path);
- if (fromBinDirectory.startsWith('..')) {
- return path;
+ if (!fromBinDirectory.startsWith('..')) {
+ return join(execRoot, fromBinDirectory);
+ }
+
+ // A path that realpath resolved out of a symlink-staged sandbox into the
+ // unsandboxed execroot: re-anchor its execroot-relative tail.
+ const markerIndex = path.indexOf(execRootMarker);
+ if (markerIndex !== -1) {
+ const tail = path.slice(markerIndex + execRootMarker.length);
+ const workspaceEnd = tail.indexOf(sep);
+ if (workspaceEnd !== -1) {
+ return join(execRoot, tail.slice(workspaceEnd + 1));
+ }
}
- return join(bazelExecRoot, fromBinDirectory);
+ return path;
}
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 2bc38bd12a89..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,23 +6,28 @@
* 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.
+ */
+ clear(): void {
+ this.modifiedFiles.clear();
+ this.typeScriptFileCache.clear();
+ this.loadResultCache.clear();
+ this.referencedFiles = undefined;
}
invalidate(files: Iterable): boolean {
@@ -37,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..1896ddf2dce3 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
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 58f2df2a05c8..b335a1160ba6 100644
--- a/packages/angular/build/src/tools/esbuild/bundler-context.ts
+++ b/packages/angular/build/src/tools/esbuild/bundler-context.ts
@@ -17,9 +17,7 @@ import {
context,
} from 'esbuild';
import assert from 'node:assert';
-import { realpathSync } from 'node:fs';
-import { basename, extname, join, relative, resolve } from 'node:path';
-import { toPosixPath } from '../../utils/path';
+import { basename, extname, isAbsolute, join, normalize, relative } from 'node:path';
import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest';
import {
type BuildOutputFile,
@@ -35,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[];
};
@@ -65,17 +79,18 @@ export class BundlerContext {
#disposed = false;
#optionsFactory: BundlerOptionsFactory;
#shouldCacheResult: boolean;
- #loadCache?: MemoryLoadResultCache;
- #realWorkspaceRoot?: string;
+ #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) => {
@@ -89,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);
@@ -102,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();
@@ -129,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();
@@ -156,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,
};
}
@@ -206,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);
@@ -221,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);
@@ -245,34 +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);
- }
- }
- }
- }
- }
-
- // esbuild always resolves its working directory through symbolic links (including
- // Windows directory junctions) and generates metafile paths relative to the resolved
- // path. When `preserveSymlinks` is enabled, the workspace root is intentionally not
- // resolved, and the metafile paths are then relative to a different base directory.
- // The paths are remapped so that all downstream consumers can rely on the documented
- // invariant that metafile paths are relative to the workspace root.
- this.#realWorkspaceRoot ??= realpathSync(this.workspaceRoot);
- if (this.#realWorkspaceRoot !== this.workspaceRoot) {
- remapMetafileBasePath(result.metafile, this.#realWorkspaceRoot, this.workspaceRoot);
}
// Update files that should be watched.
@@ -281,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,
@@ -430,9 +443,8 @@ export class BundlerContext {
...result,
outputFiles,
initialFiles,
- externalImports: {
- [isPlatformServer ? 'server' : 'browser']: externalImports,
- },
+ externalImports,
+ platform: isPlatformServer ? 'server' : 'browser',
externalConfiguration,
errors: undefined,
};
@@ -442,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));
}
}
}
@@ -460,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) {
@@ -501,71 +576,6 @@ export class BundlerContext {
}
}
-/**
- * Remaps all relative paths within an esbuild metafile from one base directory to another.
- * Virtual files (e.g., `angular:` namespaced or bundler generated), external imports, and
- * non-relative paths are left unmodified.
- *
- * @param metafile The metafile to update in place.
- * @param fromBase The absolute base directory the metafile paths are currently relative to.
- * @param toBase The absolute base directory the metafile paths should be made relative to.
- */
-export function remapMetafileBasePath(metafile: Metafile, fromBase: string, toBase: string): void {
- const remapped = new Map();
- const remap = (value: string): string => {
- // Skip virtual files and paths with a scheme-like or namespace prefix (e.g., `angular:`)
- if (
- isInternalAngularFile(value) ||
- isInternalBundlerFile(value) ||
- /^[^\\/.]{2,}:/.test(value)
- ) {
- return value;
- }
-
- let result = remapped.get(value);
- if (result === undefined) {
- // esbuild metafile paths always use POSIX path separators
- result = toPosixPath(relative(toBase, resolve(fromBase, value)));
- remapped.set(value, result);
- }
-
- return result;
- };
-
- const inputs: Metafile['inputs'] = {};
- for (const [key, value] of Object.entries(metafile.inputs)) {
- for (const importRecord of value.imports) {
- if (!importRecord.external) {
- importRecord.path = remap(importRecord.path);
- }
- }
- inputs[remap(key)] = value;
- }
- metafile.inputs = inputs;
-
- const outputs: Metafile['outputs'] = {};
- for (const [key, value] of Object.entries(metafile.outputs)) {
- if (value.entryPoint !== undefined) {
- value.entryPoint = remap(value.entryPoint);
- }
- if (value.cssBundle !== undefined) {
- value.cssBundle = remap(value.cssBundle);
- }
- for (const importRecord of value.imports) {
- if (!importRecord.external) {
- importRecord.path = remap(importRecord.path);
- }
- }
- const outputInputs: (typeof value)['inputs'] = {};
- for (const [inputKey, inputValue] of Object.entries(value.inputs)) {
- outputInputs[remap(inputKey)] = inputValue;
- }
- value.inputs = outputInputs;
- outputs[remap(key)] = value;
- }
- metafile.outputs = outputs;
-}
-
function isInternalAngularFile(file: string) {
return file.startsWith('angular:');
}
diff --git a/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts b/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts
index 8806f1d90406..213354389122 100644
--- a/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts
+++ b/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts
@@ -6,94 +6,108 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import type { Metafile } from 'esbuild';
-import { join, relative } from 'node:path';
-import { remapMetafileBasePath } from './bundler-context';
-
-describe('remapMetafileBasePath', () => {
- // Simulates a workspace root accessed through a symbolic link or Windows
- // directory junction (`toBase`) that resolves to a different real path
- // (`fromBase`), as esbuild resolves its working directory through links.
- const fromBase = join('/real', 'projects', 'demo');
- const toBase = join('/linked', 'demo');
-
- /** Creates a metafile path as esbuild would: relative to the resolved (real) base. */
- const fromBaseRelative = (filePath: string): string => relative(fromBase, join(toBase, filePath));
-
- it('remaps input and output paths onto the target base directory', () => {
- const metafile: Metafile = {
- inputs: {
- [fromBaseRelative('src/main.ts')]: { bytes: 10, imports: [] },
- },
- outputs: {
- [fromBaseRelative('main.js')]: {
- bytes: 100,
- inputs: { [fromBaseRelative('src/main.ts')]: { bytesInOutput: 10 } },
- imports: [{ path: fromBaseRelative('chunk-ABC.js'), kind: 'import-statement' }],
- exports: [],
- entryPoint: fromBaseRelative('src/main.ts'),
- cssBundle: fromBaseRelative('main.css'),
- },
- },
- };
-
- remapMetafileBasePath(metafile, fromBase, toBase);
-
- expect(Object.keys(metafile.inputs)).toEqual(['src/main.ts']);
- expect(Object.keys(metafile.outputs)).toEqual(['main.js']);
-
- const output = metafile.outputs['main.js'];
- expect(output.entryPoint).toBe('src/main.ts');
- expect(output.cssBundle).toBe('main.css');
- expect(Object.keys(output.inputs)).toEqual(['src/main.ts']);
- expect(output.imports[0].path).toBe('chunk-ABC.js');
- });
+import { BundlerContext } from './bundler-context';
+import { MemoryLoadResultCache } from './load-result-cache';
- it('does not modify virtual and namespaced files', () => {
- const metafile: Metafile = {
- inputs: {
- 'angular:polyfills': {
- bytes: 10,
- imports: [{ path: '', kind: 'import-statement' }],
- },
- },
- outputs: {
- [fromBaseRelative('polyfills.js')]: {
- bytes: 100,
- inputs: { 'angular:polyfills': { bytesInOutput: 10 } },
- imports: [],
- exports: [],
- entryPoint: 'angular:polyfills',
- },
- },
- };
-
- remapMetafileBasePath(metafile, fromBase, toBase);
-
- expect(Object.keys(metafile.inputs)).toEqual(['angular:polyfills']);
- expect(metafile.inputs['angular:polyfills'].imports[0].path).toBe('');
-
- const output = metafile.outputs['polyfills.js'];
- expect(output.entryPoint).toBe('angular:polyfills');
- expect(Object.keys(output.inputs)).toEqual(['angular:polyfills']);
- });
+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`);
+ }
- it('does not modify external imports', () => {
- const externalPath = 'https://example.com/module.js';
- const metafile: Metafile = {
- inputs: {},
- outputs: {
- [fromBaseRelative('main.js')]: {
- bytes: 100,
- inputs: {},
- imports: [{ path: externalPath, kind: 'import-statement', external: true }],
- exports: [],
- },
- },
- };
-
- remapMetafileBasePath(metafile, fromBase, toBase);
-
- expect(metafile.outputs['main.js'].imports[0].path).toBe(externalPath);
+ expect(context.invalidate(new Set(['/workspace/src/file50.css']))).toBeTrue();
+ });
});
});
diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts
index bebfd6a3eedd..53e17e7e427c 100644
--- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts
+++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts
@@ -194,11 +194,21 @@ export class ExecutionResult {
return changed;
}
- async dispose(): Promise {
- await Promise.allSettled([
- ...this.rebuildContexts.typescriptContexts.map((context) => context.dispose()),
- ...this.rebuildContexts.otherContexts.map((context) => context.dispose()),
- this.componentStyleBundler.dispose(),
- ]);
+ #disposal?: Promise;
+
+ dispose(): Promise {
+ return (this.#disposal ??= this.#dispose());
+ }
+
+ async #dispose(): Promise {
+ try {
+ await Promise.allSettled([
+ ...this.rebuildContexts.typescriptContexts.map((context) => context.dispose()),
+ ...this.rebuildContexts.otherContexts.map((context) => context.dispose()),
+ this.componentStyleBundler.dispose(),
+ ]);
+ } finally {
+ this.codeBundleCache?.clear();
+ }
}
}
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 71a055cea676..0ad9262da354 100644
--- a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
+++ b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
@@ -6,11 +6,14 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import remapping, { type EncodedSourceMap, type SourceMapInput } from '@ampproject/remapping';
+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 { createSharedTranslationProxy } from './i18n-translation-reader';
/**
* The options passed to the inliner for each file request
@@ -28,9 +31,11 @@ interface InlineFileRequest {
locale: string;
/**
- * The translation messages for the locale that should be used during the inlining process of the file.
+ * The serialized translation messages for the locale that should be used during the inlining
+ * 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?: Record;
+ translation?: Blob | SharedArrayBuffer;
}
/**
@@ -53,19 +58,155 @@ interface InlineCodeRequest {
locale: string;
/**
- * The translation messages for the locale that should be used during the inlining process of the file.
+ * The serialized translation messages for the locale that should be used during the inlining
+ * 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?: Record;
+ 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
-// TODO: Evaluate overall performance difference of passing translations here as well
-const { files, missingTranslation, shouldOptimize } = (workerData || {}) as {
+const { files, missingTranslation, translations } = (workerData || {}) as {
files: ReadonlyMap;
missingTranslation: 'error' | 'warning' | 'ignore';
- shouldOptimize: boolean;
+ translations?: ReadonlyMap;
};
+/**
+ * Cached file data including code and extracted localization metadata.
+ */
+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 or wraps the translation messages for a locale, reusing the result for any
+ * subsequent request that targets the same locale.
+ * @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(
+ locale: string,
+ translation?: Blob | SharedArrayBuffer,
+): Promise> | undefined {
+ const translationData = translation ?? translations?.get(locale);
+ if (!translationData) {
+ return undefined;
+ }
+
+ let messagesPromise = deserializedTranslations.get(locale);
+ if (!messagesPromise) {
+ if (translationData instanceof Blob) {
+ messagesPromise = translationData
+ .arrayBuffer()
+ .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 messagesPromise;
+}
+
/**
* Inlines the provided locale and translation into a JavaScript file that contains `$localize` usage.
* This function is the main entry for the Worker's action that is called by the worker pool.
@@ -74,13 +215,21 @@ const { files, missingTranslation, shouldOptimize } = (workerData || {}) as {
* @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(code, map && (JSON.parse(map) as SourceMapInput), request);
+ const result = await inlineLocalize(
+ code,
+ map,
+ metadata,
+ request.locale,
+ await loadTranslation(request.locale, request.translation),
+ request.filename,
+ );
return {
file: request.filename,
@@ -90,6 +239,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.
@@ -98,7 +300,15 @@ export default async function inlineFile(request: InlineFileRequest) {
* @returns An object containing the inlined code.
*/
export async function inlineCode(request: InlineCodeRequest) {
- const result = await transformWithOxc(request.code, undefined, request);
+ const metadata = extractLocalizeMetadata(request.filename, request.code);
+ const result = await inlineLocalize(
+ request.code,
+ undefined,
+ metadata,
+ request.locale,
+ await loadTranslation(request.locale, request.translation),
+ request.filename,
+ );
return {
output: result.code,
@@ -132,89 +342,218 @@ 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.
- * @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,
-) {
- 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,
- options.translation || {},
- messageParts,
- node.quasi.expressions.map((_, index) => index),
- options.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);
+ }
+ }
+
+ for (const site of metadata.localeInsertSites) {
+ magicString.overwrite(site.start, site.end, JSON.stringify(locale));
+ }
+
+ 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;
if (map && magicString.hasChanged()) {
- const rawMap = magicString.generateMap({
- source: options.filename,
+ // A decoded map is generated here rather than an encoded one because remapping decodes its
+ // 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: filename,
includeContent: true,
hires: 'boundary',
});
- outputMap = remapping([rawMap as EncodedSourceMap, map], () => null);
+ outputMap = remapping([{ ...rawMap, version: 3 } satisfies DecodedSourceMap, map], () => null);
}
return {
diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
index af13226a68d4..41de55de666f 100644
--- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
+++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
@@ -7,11 +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.
@@ -19,6 +21,36 @@ 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 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 SharedArrayBuffer or Blob containing the serialized messages, or undefined if none.
+ */
+function serializeTranslation(
+ translation: Record | undefined,
+): SharedArrayBuffer | Blob | undefined {
+ if (!translation) {
+ return undefined;
+ }
+
+ if (typeof SharedArrayBuffer !== 'undefined') {
+ return encodeTranslationToBuffer(translation);
+ }
+
+ return new Blob([serialize(translation)]);
+}
+
/**
* Inlining options that should apply to all transformed code.
*/
@@ -27,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;
}
/**
@@ -38,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;
@@ -47,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 = [];
@@ -99,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
@@ -111,103 +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