diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml
index 94fdfb8df840..f21e2612ef53 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 57b31cb9c2f1..eb33533f00e2 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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
index f95ab2f4ac05..15fc3e9c9ba5 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -23,12 +23,12 @@ jobs:
with:
persist-credentials: false
- name: Initialize CodeQL
- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
+ uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
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
+ uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
category: '/language:javascript-typescript'
diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml
index e0efdf2b94e4..2c9e701b95e2 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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..591414f2e95a 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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..ad9364286682 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel
- uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/setup@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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@04230133d395dfb032d782b8e63b4fcbbd406aa5 # main
- name: Setup Bazel RBE
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17
+ uses: angular/dev-infra/github-actions/bazel/configure-remote@04230133d395dfb032d782b8e63b4fcbbd406aa5 # 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
index 2e9f8b03919a..862ead79f856 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: 'Run analysis'
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
+ uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
@@ -46,6 +46,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
- uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
+ uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
sarif_file: results.sarif
diff --git a/.nvmrc b/.nvmrc
index f9e7451e7715..c94711948a66 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.23.1
+22.23.2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8867117ff498..c624abd370a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,50 +1,199 @@
-
+
-# 22.0.8 (2026-07-23)
+# 22.1.4 (2026-08-13)
### @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 |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| [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 |
| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- |
-| [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 |
+| [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 |
-
+
-# 22.1.0-next.4 (2026-07-15)
+# 22.1.3 (2026-08-05)
### @angular/cli
-| 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 |
+| 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 |
+
+
+
+
+
+# 22.1.2 (2026-07-30)
+
+### @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 |
+| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ |
+| [baed978885](https://github.com/angular/angular-cli/commit/baed978885585fceadc64f0151d7474054d57d6a) | fix | prevent IIFE wrapper interleaving for adjacent classes in minified files |
+
+
+
+
+
+# 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.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 |
-| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- |
-| [b345bb54d](https://github.com/angular/angular-cli/commit/b345bb54d3ec7c73f9db70400f823d90aeab8fa8) | fix | remove unused spec tsconfig outDir |
+| 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 |
-| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ |
-| [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 |
+| 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 |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
+| [7ef9ed24d](https://github.com/angular/angular-cli/commit/7ef9ed24d3c111474ae21c06cb2b05a21804f586) | feat | support the standard Forwarded header |
+
+
+
+
+
+# 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 |
+| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- |
+| [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 |
@@ -85,30 +234,6 @@
-
-
-# 22.1.0-next.3 (2026-07-09)
-
-### @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 |
-
-### @angular/ssr
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- |
-| [0bd22dae0](https://github.com/angular/angular-cli/commit/0bd22dae09a83e36ed3863ca2b050666cc8d7aec) | fix | replace all route parameters when resolving relative redirects |
-
-
-
# 22.0.6 (2026-07-08)
@@ -129,47 +254,6 @@
-
-
-# 22.1.0-next.2 (2026-07-01)
-
-### @angular/cli
-
-| 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 |
-
-### @schematics/angular
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- |
-| [0b3172e07](https://github.com/angular/angular-cli/commit/0b3172e076b75df5b8c3066a115d1151f8b0bad9) | fix | remove unused app tsconfig outDir |
-
-### @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 |
-
-### @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 |
-
-### @angular/ssr
-
-| Commit | Type | Description |
-| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- |
-| [d9fb854c9](https://github.com/angular/angular-cli/commit/d9fb854c9a48ba8852d57f3fa8f7a9640ee0aab2) | fix | preserve response headers during redirect |
-
-
-
# 22.0.5 (2026-07-01)
@@ -247,43 +331,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 +449,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..1c6965292e88 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 = "20a373d609c4f5765b9ad367a205f3a635dd2cda",
remote = "https://github.com/angular/rules_angular.git",
)
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
- commit = "daa0b5ecd15668d12163f70d4c9836ada70808c9",
+ commit = "04230133d395dfb032d782b8e63b4fcbbd406aa5",
remote = "https://github.com/angular/dev-infra.git",
)
bazel_dep(name = "rules_browsers")
git_override(
module_name = "rules_browsers",
- commit = "3a6bd40f9be945dc51c976a21424c656448f9b09",
+ commit = "37853f23de9a9a70f53c02a9baa27e08d7d12003",
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"),
+ "22.23.2-darwin_arm64": ("node-v22.23.2-darwin-arm64.tar.gz", "node-v22.23.2-darwin-arm64", "61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6"),
+ "22.23.2-darwin_amd64": ("node-v22.23.2-darwin-x64.tar.gz", "node-v22.23.2-darwin-x64", "58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026"),
+ "22.23.2-linux_arm64": ("node-v22.23.2-linux-arm64.tar.xz", "node-v22.23.2-linux-arm64", "fff4078c5def658577f92c88db7db3bc0072924bfb93fe52c1e744a54e94abb8"),
+ "22.23.2-linux_ppc64le": ("node-v22.23.2-linux-ppc64le.tar.xz", "node-v22.23.2-linux-ppc64le", "e2ba10110ad34096c051fe47134fcd12175ecc48e7e879d7d4c78b70cf66442c"),
+ "22.23.2-linux_s390x": ("node-v22.23.2-linux-s390x.tar.xz", "node-v22.23.2-linux-s390x", "50aa0935c7caee2f95434f84c2c19f16e4f223257eadb341a3a2d5aaa545bbe6"),
+ "22.23.2-linux_amd64": ("node-v22.23.2-linux-x64.tar.xz", "node-v22.23.2-linux-x64", "d60acfe00a2932254bb0ad20e01b0d74397a0875595de719654b214f4b03f307"),
+ "22.23.2-windows_amd64": ("node-v22.23.2-win-x64.zip", "node-v22.23.2-win-x64", "1177b4137ba5adaa56354ae40f1080c7450e8ae09cecb47da459d1c52ac99f97"),
},
- node_version = "22.23.1",
+ node_version = "22.23.2",
)
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.21.0",
+ pnpm_version_integrity = "sha512-UhcFvOaJkk6scvWjWHEi82JonvZXHlW6gAdv1jfBETLs/62ib61Op5xIW/3b/T1aKlsFgFp36JPeceyKbMo7sQ==",
)
use_repo(pnpm, "pnpm")
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index eb446604633b..4542fcb3d759 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -19,15 +19,18 @@
"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_js/3.3.1/MODULE.bazel": "3e02b51b503ba8dda69b043290f6cc11add9aeb8db0bf1f6c861c396c7ddc5b2",
+ "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_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_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",
@@ -52,8 +55,9 @@
"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 +73,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",
@@ -171,7 +176,8 @@
"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",
@@ -222,8 +228,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 +444,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 +455,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 +545,7 @@
},
"@@rules_browsers+//browsers:extensions.bzl%browsers": {
"general": {
- "bzlTransitiveDigest": "3W9m9j39szqkOoSVVqamCaSwCCw7x5Wq1kcYTwYBNOc=",
+ "bzlTransitiveDigest": "4TUvIB8juKCShOmy6m77drIdimJqOTEPwfzMxkWD5a4=",
"usagesDigest": "FmXYJVoVJlnfUU8x8gObSvu4qWcco/9Faw61aC/wBF0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -548,9 +554,9 @@
"rules_browsers_chrome_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "13d6638183ccab406ee16e28730826bd3ea57fe564a741bc935401221dfec64f",
+ "sha256": "5b132ebe0be5c0c15cb5222c33e04e37ae21ed3e00d84c68e905f80f29221c50",
"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/153.0.7993.0/linux64/chrome-headless-shell-linux64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-linux64/chrome-headless-shell"
@@ -566,9 +572,9 @@
"rules_browsers_chrome_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "b50ee6ddb3bf30c91f3094fa41cc8e8de60e317c63ef058db2375346cde78d98",
+ "sha256": "522e62dbfce61fddd413fb76a65e699b6f5bdce4045e4398d1e2fb1ef73ff6f3",
"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/153.0.7993.0/mac-x64/chrome-headless-shell-mac-x64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-x64/chrome-headless-shell"
@@ -584,9 +590,9 @@
"rules_browsers_chrome_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "49859c1d3b0d29a82c58765e9e5fe21efa0bf7c03a86fc91eb5f3f94f34c45b6",
+ "sha256": "b68d54b63ab30042874a87ad81a135360fa87e1d4763df2431b30b4d59878b4e",
"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/153.0.7993.0/mac-arm64/chrome-headless-shell-mac-arm64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-arm64/chrome-headless-shell"
@@ -602,9 +608,9 @@
"rules_browsers_chrome_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "d161d88a999effd212ce87df2ff710be1a25a495cc428d4c3029feccd02dafe9",
+ "sha256": "0fd5b488d41686cb7cb0be1feb861f1e4cb44d9c5e67a4c5328a385742aa6a8e",
"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/153.0.7993.0/win64/chrome-headless-shell-win64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-win64/chrome-headless-shell.exe"
@@ -620,9 +626,9 @@
"rules_browsers_chromedriver_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "7bb2eedf7030523ba617704bf6c3e472b5974d048d7fa6ac6a764904556c6f36",
+ "sha256": "69139f654d93c12d04209713c725b33ea7074a1e35c47aaf98f419c4a00b4e9b",
"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/153.0.7993.0/linux64/chromedriver-linux64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-linux64/chromedriver"
@@ -636,9 +642,9 @@
"rules_browsers_chromedriver_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "5c1fffef90ecea2bb0c12c5a9c0549116fc7acda13cce15881da4aa8b74e6794",
+ "sha256": "b56b49cb41658c25405757b3c01a5412403e3744ee6935c5f9f59bf2b907bcf6",
"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/153.0.7993.0/mac-x64/chromedriver-mac-x64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-mac-x64/chromedriver"
@@ -652,9 +658,9 @@
"rules_browsers_chromedriver_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "a33d54c99a50fef25cbedb7c3d3cdd838dd784556138648bcc5da5814109a306",
+ "sha256": "e2bd843b45ba197632d29eeef188940488779684207eb3118a5a78c26aef9659",
"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/153.0.7993.0/mac-arm64/chromedriver-mac-arm64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-mac-arm64/chromedriver"
@@ -668,9 +674,9 @@
"rules_browsers_chromedriver_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "2673d6a87dabd50c516d4d69b4daf742ff6169877c04bb226971503012262d8c",
+ "sha256": "2f56f10ba3989d8b5c44c22cff080c2cc436314f8f8babbcc151bb95b34de841",
"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/153.0.7993.0/win64/chromedriver-win64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-win64/chromedriver.exe"
@@ -684,9 +690,9 @@
"rules_browsers_firefox_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "748704b06ffaabc8bd2b3351c93c244db4e29bfe21af6ecb4d6c124c7bd4a234",
+ "sha256": "bfc57e7b6b4e6204b11e7e03c4b93cff708e9fb37f6b9948be243455311d82ee",
"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/153.0/linux-x86_64/en-US/firefox-153.0.tar.xz"
],
"named_files": {
"FIREFOX": "firefox/firefox"
@@ -700,9 +706,9 @@
"rules_browsers_firefox_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "b8a46188850d2fb32f16ad3c0829b08cd689bc714b8ee18fd9b09bb60629004d",
+ "sha256": "2f9b5a20e546e7e79e4182f8fe10353a3e251635963ab3f6d399a3f290adeb96",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/152.0/mac/en-US/Firefox%20152.0.dmg"
+ "https://archive.mozilla.org/pub/firefox/releases/153.0/mac/en-US/Firefox%20153.0.dmg"
],
"named_files": {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox"
@@ -716,9 +722,9 @@
"rules_browsers_firefox_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "b8a46188850d2fb32f16ad3c0829b08cd689bc714b8ee18fd9b09bb60629004d",
+ "sha256": "2f9b5a20e546e7e79e4182f8fe10353a3e251635963ab3f6d399a3f290adeb96",
"urls": [
- "https://archive.mozilla.org/pub/firefox/releases/152.0/mac/en-US/Firefox%20152.0.dmg"
+ "https://archive.mozilla.org/pub/firefox/releases/153.0/mac/en-US/Firefox%20153.0.dmg"
],
"named_files": {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox"
@@ -732,9 +738,9 @@
"rules_browsers_firefox_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
- "sha256": "3c426623ff83cb014402607c7ec99fea5b39eec2e44a07f639680c393ff9784e",
+ "sha256": "b409ff5af3419214f11f928e67b1f44c1ad0f8a46f7db7e697caedefa02a6d2a",
"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/153.0/win64/en-US/Firefox%20Setup%20153.0.exe"
],
"named_files": {
"FIREFOX": "core/firefox.exe"
@@ -1175,7 +1181,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..88f85d3477b0 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.1.0"
+ANGULAR_FW_VERSION = "^22.1.0"
+ANGULAR_FW_PEER_DEP = "^22.0.0"
+NG_PACKAGR_PEER_DEP = "^22.0.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/modules/testing/builder/package.json b/modules/testing/builder/package.json
index 9aede9dc7d10..048981abdc06 100644
--- a/modules/testing/builder/package.json
+++ b/modules/testing/builder/package.json
@@ -8,7 +8,7 @@
"browser-sync": "3.0.4",
"istanbul-lib-instrument": "6.0.3",
"jsdom": "29.1.1",
- "ng-packagr": "22.1.0-next.3",
+ "ng-packagr": "22.1.1",
"rxjs": "7.8.2",
"vitest": "4.1.10"
}
diff --git a/package.json b/package.json
index f67f33227c76..2e8829bf5fd4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@angular/devkit-repo",
- "version": "22.1.0-next.4",
+ "version": "22.1.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.21.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.21.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.1.1",
"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.1.1",
+ "@angular/cdk": "22.1.2",
+ "@angular/common": "22.1.1",
+ "@angular/compiler": "22.1.1",
+ "@angular/core": "22.1.1",
+ "@angular/forms": "22.1.1",
+ "@angular/localize": "22.1.1",
+ "@angular/material": "22.1.2",
+ "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#c71d9b6af7560faa3d002534d416a8045111adae",
+ "@angular/platform-browser": "22.1.1",
+ "@angular/platform-server": "22.1.1",
+ "@angular/router": "22.1.1",
+ "@angular/service-worker": "22.1.1",
"@babel/core": "8.0.1",
"@bazel/bazelisk": "1.28.1",
"@bazel/buildifier": "8.2.1",
@@ -96,8 +96,8 @@
"@typescript-eslint/parser": "8.64.0",
"ajv": "8.20.0",
"buffer": "6.0.3",
- "esbuild": "0.28.1",
- "esbuild-wasm": "0.28.1",
+ "esbuild": "0.28.2",
+ "esbuild-wasm": "0.28.2",
"eslint": "10.7.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel
index 7325ec88d35b..5a6f811e3ee5 100644
--- a/packages/angular/build/BUILD.bazel
+++ b/packages/angular/build/BUILD.bazel
@@ -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..e37ee6976688 100644
--- a/packages/angular/build/package.json
+++ b/packages/angular/build/package.json
@@ -27,13 +27,13 @@
"@vitejs/plugin-basic-ssl": "2.3.0",
"beasties": "0.4.3",
"browserslist": "^4.26.0",
- "esbuild": "0.28.1",
+ "esbuild": "0.28.2",
"https-proxy-agent": "9.1.0",
"jsonc-parser": "3.3.1",
"listr2": "10.2.2",
"magic-string": "1.0.0",
"mrmime": "2.0.1",
- "oxc-parser": "0.140.0",
+ "oxc-parser": "0.142.0",
"parse5-html-rewriting-stream": "8.0.1",
"picomatch": "4.0.5",
"piscina": "5.2.0",
@@ -55,8 +55,8 @@
"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",
+ "ng-packagr": "22.1.1",
+ "postcss": "8.5.25",
"rollup": "4.62.2",
"rxjs": "7.8.2",
"vitest": "4.1.10"
diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts
index d7dbdc8cfe21..dbec8d687b9f 100644
--- a/packages/angular/build/src/builders/application/build-action.ts
+++ b/packages/angular/build/src/builders/application/build-action.ts
@@ -81,7 +81,7 @@ export async function* runEsBuildBuildAction(
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 +89,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();
}
}
diff --git a/packages/angular/build/src/builders/application/chunk-optimizer.ts b/packages/angular/build/src/builders/application/chunk-optimizer.ts
index 7d7a6e56b829..2241a4204999 100644
--- a/packages/angular/build/src/builders/application/chunk-optimizer.ts
+++ b/packages/angular/build/src/builders/application/chunk-optimizer.ts
@@ -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..53aaec882cbf 100644
--- a/packages/angular/build/src/builders/application/execute-build.ts
+++ b/packages/angular/build/src/builders/application/execute-build.ts
@@ -18,11 +18,8 @@ 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 {
- calculateEstimatedTransferSizes,
- logBuildStats,
- transformSupportedBrowsersToTargets,
-} from '../../tools/esbuild/utils';
+import { transformSupportedBrowsersToTargets } from '../../tools/esbuild/target';
+import { calculateEstimatedTransferSizes, logBuildStats } from '../../tools/esbuild/utils';
import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator';
import { optimizeChunksThreshold } from '../../utils/environment-options';
import { resolveAssets } from '../../utils/resolve-assets';
@@ -74,12 +71,19 @@ export async function executeBuild(
let bundlingResult: 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();
@@ -122,6 +126,12 @@ export async function executeBuild(
angularCompilationContext,
templateUpdates,
);
+ executionResult = new ExecutionResult(
+ bundlerContexts,
+ componentStyleBundler,
+ codeBundleCache,
+ templateUpdates,
+ );
// Bundle everything on initial build
bundlingResult = await BundlerContext.bundleAll([
@@ -138,234 +148,241 @@ export async function executeBuild(
const componentResults = await componentStyleBundler.bundleAllFiles(true, true);
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...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);
-
- 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) {
+ executionResult.addErrors(bundlingResult.errors);
- bundlingResult = optimizationResult;
+ 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 { 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,
+ ),
+ );
+
+ if (optimizationResult.errors) {
+ executionResult.addErrors(optimizationResult.errors);
+
+ return executionResult;
+ }
- if (exclusions.has(dep)) {
- return true;
+ bundlingResult = optimizationResult;
}
+ }
- for (const prefix of exclusionsPrefixes) {
- if (dep.startsWith(prefix)) {
+ // 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;
+ }
+
+ if (exclusions.has(dep)) {
return true;
}
- }
- return false;
- };
+ for (const prefix of exclusionsPrefixes) {
+ if (dep.startsWith(prefix)) {
+ return true;
+ }
+ }
- const implicitBrowser: string[] = [];
- for (const dep of browser) {
- if (isExplicitExternal(dep)) {
- explicitExternal.add(dep);
- } else {
- implicitBrowser.push(dep);
+ return false;
+ };
+
+ 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);
+ const implicitServer: string[] = [];
+ for (const dep of server) {
+ if (isExplicitExternal(dep)) {
+ explicitExternal.add(dep);
+ } else {
+ implicitServer.push(dep);
+ }
}
- }
- executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]);
- }
+ executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]);
+ }
- const { metafile, initialFiles, outputFiles } = bundlingResult;
+ const { metafile, initialFiles, outputFiles } = bundlingResult;
- executionResult.outputFiles.push(...outputFiles);
+ executionResult.outputFiles.push(...outputFiles);
- // Analyze files for bundle budget failures if present
- let budgetFailures: BudgetCalculatorResult[] | undefined;
- if (options.budgets) {
- const compatStats = generateBudgetStats(metafile, outputFiles, initialFiles);
- budgetFailures = [...checkBudgets(options.budgets, compatStats, true)];
- for (const { message, severity } of budgetFailures) {
- if (severity === 'error') {
- executionResult.addError(message);
- } else {
- executionResult.addWarning(message);
+ // 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);
+ }
}
}
- }
- // Calculate estimated transfer size if scripts are optimized
- let estimatedTransferSizes;
- if (optimizationOptions.scripts || optimizationOptions.styles.minify) {
- estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles);
- }
-
- // Check metafile for CommonJS module usage if optimizing scripts
- if (optimizationOptions.scripts) {
- const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies);
- executionResult.addWarnings(messages);
- }
+ // Calculate estimated transfer size if scripts are optimized
+ let estimatedTransferSizes;
+ if (optimizationOptions.scripts || optimizationOptions.styles.minify) {
+ estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles);
+ }
- // Copy assets
- if (assets) {
- executionResult.addAssets(await resolveAssets(assets, workspaceRoot));
- }
+ // Check metafile for CommonJS module usage if optimizing scripts
+ if (optimizationOptions.scripts) {
+ const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies);
+ executionResult.addWarnings(messages);
+ }
- // Extract and write licenses for used packages
- if (options.extractLicenses) {
- executionResult.addOutputFile(
- '3rdpartylicenses.txt',
- await extractLicenses(metafile, workspaceRoot),
- BuildOutputFileType.Root,
- );
- }
+ // Copy assets
+ if (assets) {
+ executionResult.addAssets(await resolveAssets(assets, workspaceRoot));
+ }
- // 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;
- }
+ // Extract and write licenses for used packages
+ if (options.extractLicenses) {
+ executionResult.addOutputFile(
+ '3rdpartylicenses.txt',
+ await extractLicenses(metafile, workspaceRoot),
+ BuildOutputFileType.Root,
+ );
+ }
- // Create server app engine manifest
- if (serverEntryPoint) {
- executionResult.addOutputFile(
- SERVER_APP_ENGINE_MANIFEST_FILENAME,
- generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref),
- BuildOutputFileType.ServerRoot,
- );
- }
+ // 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;
+ }
- // 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,
- );
+ // Create server app engine manifest
+ if (serverEntryPoint) {
+ executionResult.addOutputFile(
+ SERVER_APP_ENGINE_MANIFEST_FILENAME,
+ generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref),
+ BuildOutputFileType.ServerRoot,
+ );
+ }
- // Deduplicate and add errors and warnings
- executionResult.addErrors([...new Set(result.errors)]);
- executionResult.addWarnings([...new Set(result.warnings)]);
+ // 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,
+ );
- executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
- executionResult.outputFiles.push(...result.additionalOutputFiles);
- executionResult.assetFiles.push(...result.additionalAssets);
- }
+ // Deduplicate and add errors and warnings
+ executionResult.addErrors([...new Set(result.errors)]);
+ executionResult.addWarnings([...new Set(result.warnings)]);
- executionResult.addOutputFile(
- 'prerendered-routes.json',
- JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2),
- BuildOutputFileType.Root,
- );
+ executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
+ executionResult.outputFiles.push(...result.additionalOutputFiles);
+ executionResult.assetFiles.push(...result.additionalAssets);
+ }
- // Write metafile if stats option is enabled
- if (options.stats) {
executionResult.addOutputFile(
- 'stats.json',
- JSON.stringify(metafile, null, 2),
+ 'prerendered-routes.json',
+ JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2),
BuildOutputFileType.Root,
);
- }
- if (!jsonLogs && !options.quiet) {
- const changedFiles =
- rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
- executionResult.addLog(
- logBuildStats(
- metafile,
- outputFiles,
- initialFiles,
- budgetFailures,
- colors,
- changedFiles,
- estimatedTransferSizes,
- !!ssrOptions,
- verbose,
- ),
- );
- }
+ // Write metafile if stats option is enabled
+ if (options.stats) {
+ executionResult.addOutputFile(
+ 'stats.json',
+ JSON.stringify(metafile, null, 2),
+ BuildOutputFileType.Root,
+ );
+ }
+
+ if (!jsonLogs && !options.quiet) {
+ const changedFiles =
+ rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
+ executionResult.addLog(
+ logBuildStats(
+ metafile,
+ outputFiles,
+ initialFiles,
+ budgetFailures,
+ colors,
+ changedFiles,
+ estimatedTransferSizes,
+ !!ssrOptions,
+ verbose,
+ ),
+ );
+ }
- return executionResult;
+ return executionResult;
+ } catch (error) {
+ await Promise.allSettled([angularCompilationContext?.dispose(), executionResult?.dispose()]);
+
+ throw error;
+ }
}
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..79209ba0eaf9 100644
--- a/packages/angular/build/src/builders/application/setup-bundling.ts
+++ b/packages/angular/build/src/builders/application/setup-bundling.ts
@@ -19,7 +19,7 @@ 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 { getSupportedNodeTargets } from '../../tools/esbuild/target';
import type { NormalizedApplicationBuildOptions } from './options';
/**
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/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/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..868590edab95 100644
--- a/packages/angular/build/src/builders/dev-server/vite/server.ts
+++ b/packages/angular/build/src/builders/dev-server/vite/server.ts
@@ -104,6 +104,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 +112,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)
@@ -214,6 +216,7 @@ export async function setupServer(
prebundleLoaderExtensions,
thirdPartySourcemaps,
define,
+ target,
),
plugins: [
createAngularSetupMiddlewaresPlugin({
@@ -239,6 +242,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/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..7618ca1e3dcb 100644
--- a/packages/angular/build/src/private.ts
+++ b/packages/angular/build/src/private.ts
@@ -35,7 +35,7 @@ export {
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/babel/plugins/adjust-static-class-members_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts
index af28ab66b264..7fe8c68030fc 100644
--- a/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts
+++ b/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts
@@ -998,4 +998,106 @@ describe('adjust-static-class-members oxc-transform implementation', () => {
`,
}),
);
+
+ it(
+ 'wraps adjacent class declarations without interleaving',
+ testCase({
+ input: 'class A{static s=[1]}class B{static s=[2]}',
+ expected: `
+ let A = /*#__PURE__*/ (() => {
+ class A {
+ static s = [1]
+ }
+ return A;
+ })();
+ let B = /*#__PURE__*/ (() => {
+ class B {
+ static s = [2]
+ }
+ return B;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps adjacent exported class declarations without interleaving',
+ testCase({
+ input: 'export class A{static s=[1]}export class B{static s=[2]}',
+ expected: `
+ export let A = /*#__PURE__*/ (() => {
+ class A {
+ static s = [1]
+ }
+ return A;
+ })();
+ export let B = /*#__PURE__*/ (() => {
+ class B {
+ static s = [2]
+ }
+ return B;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps adjacent default export and class declarations without interleaving',
+ testCase({
+ input: 'export default class A{static s=[1]}class B{static s=[2]}',
+ expected: `
+ let A = /*#__PURE__*/ (() => {
+ class A {
+ static s = [1]
+ }
+ return A;
+ })();
+ export { A as default };
+ let B = /*#__PURE__*/ (() => {
+ class B {
+ static s = [2]
+ }
+ return B;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'splits default export without interleaving with adjacent class declaration',
+ testCase({
+ input: 'export default class A{}class B{static s=[2]}',
+ expected: `
+ class A {}
+ export { A as default };
+ let B = /*#__PURE__*/ (() => {
+ class B {
+ static s = [2]
+ }
+ return B;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps adjacent variable class declarations without interleaving',
+ testCase({
+ input: 'let A=class A{static s=[1]};class B{static s=[2]}',
+ expected: `
+ let A = /*#__PURE__*/ (() => {
+ let A = class A {
+ static s = [1]
+ };
+ return A;
+ })();
+ let B = /*#__PURE__*/ (() => {
+ class B {
+ static s = [2]
+ }
+ return B;
+ })();
+ `,
+ }),
+ );
});
diff --git a/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts
index fc01eecf991a..82877cb3b2e3 100644
--- a/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts
+++ b/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts
@@ -280,4 +280,103 @@ describe('adjust-typescript-enums oxc-transform implementation', () => {
`,
}),
);
+
+ it(
+ 'handles TypeScript enums with chained exports assignment (angular-split / shared-docs pattern)',
+ testCase({
+ input: `
+ var Area;
+ (function (a1) {
+ a1[a1["areaAfter"] = 0] = "areaAfter";
+ a1[a1["preserveOtherCategoryOrder"] = 1] = "preserveOtherCategoryOrder";
+ })(Area || (Area = exports.Area = {}));
+ `,
+ expected: `
+ var Area = /*#__PURE__*/ (function (a1) {
+ a1[(a1["areaAfter"] = 0)] = "areaAfter";
+ a1[(a1["preserveOtherCategoryOrder"] = 1)] = "preserveOtherCategoryOrder";
+ return a1;
+ })(Area || (exports.Area = {}));
+ `,
+ }),
+ );
+
+ it(
+ 'handles TypeScript enums wrapped in parentheses',
+ testCase({
+ input: `
+ var ChangeDetectionStrategy;
+ ((function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})));
+ `,
+ expected: `
+ var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush";
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy || {});
+ `,
+ }),
+ );
+
+ it(
+ 'wraps Crockford-style TypeScript enum IIFE without leaving dangling parentheses',
+ testCase({
+ input: `
+ var HDirection;
+ (function (HDirection) {
+ HDirection[HDirection['Backwards'] = -1] = 'Backwards';
+ HDirection[HDirection['Forwards'] = 1] = 'Forwards';
+ }(HDirection || (HDirection = {})));
+ const nextStatement = true;
+ `,
+ expected: `
+ var HDirection = /*#__PURE__*/ (function (HDirection) {
+ HDirection[(HDirection['Backwards'] = -1)] = 'Backwards';
+ HDirection[(HDirection['Forwards'] = 1)] = 'Forwards';
+ return HDirection;
+ }(HDirection || {}));
+
+ const nextStatement = true;
+ `,
+ }),
+ );
+
+ it(
+ 'wraps TypeScript enum IIFE with multiple nested parentheses',
+ testCase({
+ input: `
+ var Foo;
+ (((function (Foo) {
+ Foo[Foo['A'] = 0] = 'A';
+ }(Foo || (Foo = {})))));
+ `,
+ expected: `
+ var Foo = /*#__PURE__*/ (function (Foo) {
+ Foo[(Foo['A'] = 0)] = 'A';
+ return Foo;
+ }(Foo || {}));
+ `,
+ }),
+ );
+
+ it(
+ 'wraps Crockford-style TypeScript enum IIFE with chained export assignments',
+ testCase({
+ input: `
+ var Foo;
+ (function (Foo) {
+ Foo[Foo['A'] = 0] = 'A';
+ }(Foo || (Foo = exports.Foo = {})));
+ `,
+ expected: `
+ var Foo = /*#__PURE__*/ (function (Foo) {
+ Foo[(Foo['A'] = 0)] = 'A';
+ return Foo;
+ }(Foo || (exports.Foo = {})));
+ `,
+ }),
+ );
});
diff --git a/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts b/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts
index 2c69d98cdf1a..569db927f45a 100644
--- a/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts
+++ b/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts
@@ -414,27 +414,30 @@ export function transform(filename: string, code: string, options: OxcTransformO
continue;
}
- // 1. Remove only the trailing characters/semicolon of the expression statement
+ // 1. Remove leading/trailing characters/parentheses of the expression statement
+ s.remove(nextStatement.start, nextExpr.start);
s.remove(nextExpr.end, nextStatement.end);
- markEdited(nextExpr.end, nextStatement.end);
+ markEdited(nextStatement.start, nextStatement.end);
// 2. Add return statement inside IIFE body
s.appendRight(callee.body.end - 1, `; return ${paramName};`);
// 3. Remove `Name = ` assignment in arguments if it's a simple identifier
if (rightCallArgument.left.type === 'Identifier') {
- s.overwrite(
- arg.right.start,
- arg.right.end,
- code.substring(rightCallArgument.right.start, rightCallArgument.right.end),
+ let replacement = code.substring(
+ rightCallArgument.right.start,
+ rightCallArgument.right.end,
);
+ if (unwrapParentheses(rightCallArgument.right).type === 'AssignmentExpression') {
+ replacement = `(${replacement})`;
+ }
+ s.overwrite(arg.right.start, arg.right.end, replacement);
markEdited(arg.right.start, arg.right.end);
}
// 4. Move IIFE to the var initializer
s.move(nextExpr.start, nextExpr.end, decl.id.end);
s.appendLeft(decl.id.end, ' = /*#__PURE__*/ ');
- markEdited(nextExpr.start, nextExpr.end);
}
}
@@ -584,25 +587,25 @@ export function transform(filename: string, code: string, options: OxcTransformO
// 1. Remove `export default `
s.overwrite(statement.start, classNode.start, '');
// 2. Wrap in IIFE
- s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
- s.appendRight(
+ s.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
+ s.appendLeft(
lastStatement.end,
`\nreturn ${classIdName};\n})();\nexport { ${classIdName} as default };`,
);
} else if (isExportNamed) {
// 1. Export is kept, turn `class` into `let ClassName = IIFE`
- s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
- s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`);
+ s.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
+ s.appendLeft(lastStatement.end, `\nreturn ${classIdName};\n})();`);
} else if (isVariableClass) {
// Wrap class inside init: `/*#__PURE__*/ (() => { let ClassName = class ClassName {}; return ClassName; })()`
- s.appendLeft(classNode.start, `/*#__PURE__*/ (() => {\nlet ${classIdName} = `);
+ s.appendRight(classNode.start, `/*#__PURE__*/ (() => {\nlet ${classIdName} = `);
const terminator = activeWrapPaths.length === 0 ? ';' : '';
const iifeClosing = activeWrapPaths.length === 0 ? '})()' : '})();';
- s.appendRight(lastStatement.end, `${terminator}\nreturn ${classIdName};\n${iifeClosing}`);
+ s.appendLeft(lastStatement.end, `${terminator}\nreturn ${classIdName};\n${iifeClosing}`);
} else {
// Standard ClassDeclaration
- s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
- s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`);
+ s.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
+ s.appendLeft(lastStatement.end, `\nreturn ${classIdName};\n})();`);
}
markEdited(statement.start, lastStatement.end);
@@ -612,7 +615,7 @@ export function transform(filename: string, code: string, options: OxcTransformO
} else if (isExportDefault && !hasPotentialSideEffects) {
// Splitting default export even when not wrapped
s.overwrite(statement.start, classNode.start, '');
- s.appendRight(classNode.end, `\nexport { ${classIdName} as default };`);
+ s.appendLeft(classNode.end, `\nexport { ${classIdName} as default };`);
markEdited(statement.start, classNode.end);
}
}
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..54c3f73b9f02 100644
--- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
@@ -459,7 +459,7 @@ export function createCompilerPlugin(
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/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..a408650a4f4f 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
@@ -25,6 +25,19 @@ export class SourceFileCache extends Map {
super();
}
+ /**
+ * Releases all cached content. The cached data is only needed for incremental
+ * rebuilds and can include the emitted contents of every TypeScript file in the
+ * program. The cache is repopulated if a build is performed after this is called.
+ */
+ override clear(): void {
+ super.clear();
+ this.modifiedFiles.clear();
+ this.typeScriptFileCache.clear();
+ this.loadResultCache.clear();
+ this.referencedFiles = undefined;
+ }
+
invalidate(files: Iterable): boolean {
if (files !== this.modifiedFiles) {
this.modifiedFiles.clear();
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..9f5b1ee5f1a2 100644
--- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts
+++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts
@@ -71,6 +71,12 @@ 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;
+ }
+
buildOptions.plugins ??= [];
buildOptions.plugins.push(
createWasmPlugin({ allowAsync: zoneless, cache: loadCache }),
diff --git a/packages/angular/build/src/tools/esbuild/bundler-context.ts b/packages/angular/build/src/tools/esbuild/bundler-context.ts
index 58f2df2a05c8..d3f3ca567a0f 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, join, relative } from 'node:path';
import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest';
import {
type BuildOutputFile,
@@ -66,7 +64,6 @@ export class BundlerContext {
#optionsFactory: BundlerOptionsFactory;
#shouldCacheResult: boolean;
#loadCache?: MemoryLoadResultCache;
- #realWorkspaceRoot?: string;
readonly watchFiles = new Set();
constructor(
@@ -264,17 +261,6 @@ export class BundlerContext {
}
}
- // 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.
// While this should technically not be linked to incremental mode, incremental is only
// currently enabled with watch mode where watch files are needed.
@@ -501,71 +487,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
deleted file mode 100644
index 8806f1d90406..000000000000
--- a/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts
+++ /dev/null
@@ -1,99 +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 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');
- });
-
- 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']);
- });
-
- 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);
- });
-});
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/i18n-inliner-worker.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
index 71a055cea676..7d230f479c11 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,13 @@
* 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';
/**
* The options passed to the inliner for each file request
@@ -28,9 +30,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 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;
}
/**
@@ -53,19 +57,55 @@ 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 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;
}
// 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 {
files: ReadonlyMap;
missingTranslation: 'error' | 'warning' | 'ignore';
shouldOptimize: boolean;
};
+/**
+ * The translation messages deserialized for the locale most recently requested of this Worker.
+ * Locales are inlined one at a time, so retaining only the active locale is enough to avoid
+ * deserializing the messages once per file while holding at most one set of messages in memory.
+ */
+let activeTranslation: { locale: string; messages: Promise> } | undefined;
+
+/**
+ * Deserializes the translation messages for an inline request, reusing the result for any
+ * subsequent request that targets the same locale.
+ * @param request An inline request containing the locale and its serialized messages.
+ * @returns The translation messages, or undefined if the locale has no translations.
+ */
+function loadTranslation(
+ request: InlineFileRequest | InlineCodeRequest,
+): Promise> | undefined {
+ const { locale, translation } = request;
+ if (!translation) {
+ return undefined;
+ }
+
+ if (activeTranslation?.locale !== locale) {
+ activeTranslation = {
+ locale,
+ // Deserializing within the stored promise ensures that concurrent requests for a locale
+ // share the one deserialization instead of each performing their own.
+ messages: translation
+ .arrayBuffer()
+ .then((buffer) => deserialize(new Uint8Array(buffer)) as Record),
+ };
+ }
+
+ return activeTranslation.messages;
+}
+
/**
* 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.
@@ -80,7 +120,12 @@ export default async function inlineFile(request: InlineFileRequest) {
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 transformWithOxc(
+ code,
+ map && (JSON.parse(map) as SourceMapInput),
+ request,
+ await loadTranslation(request),
+ );
return {
file: request.filename,
@@ -98,7 +143,12 @@ 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 result = await transformWithOxc(
+ request.code,
+ undefined,
+ request,
+ await loadTranslation(request),
+ );
return {
output: result.code,
@@ -131,17 +181,68 @@ async function loadLocalizeTools(): Promise {
return localizeToolsModule;
}
+/**
+ * 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.
+ */
+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]);
+ }
+}
+
/**
* Transforms a JavaScript file using OXC and Magic-String to inline the request locale and translation.
* @param code A string containing the JavaScript code to transform.
* @param map A sourcemap object for the provided JavaScript code.
* @param options The inline request options to use.
+ * @param translation The translation messages to inline, or undefined for an untranslated locale.
* @returns An object containing the code, map, and diagnostics from the transformation.
*/
async function transformWithOxc(
code: string,
map: SourceMapInput | undefined,
options: InlineFileRequest,
+ translation: Record | undefined,
) {
const { program } = parseSync(options.filename, code, {
sourceType: 'unambiguous',
@@ -155,13 +256,12 @@ async function transformWithOxc(
const { Diagnostics, translate } = await loadLocalizeTools();
const diagnostics = new Diagnostics();
- 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));
}
- },
- '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);
@@ -169,10 +269,10 @@ async function transformWithOxc(
const [translatedParts, translatedSubstitutions] = translate(
diagnostics,
- options.translation || {},
+ translation || {},
messageParts,
node.quasi.expressions.map((_, index) => index),
- options.translation === undefined ? 'ignore' : missingTranslation,
+ translation === undefined ? 'ignore' : missingTranslation,
);
// Reconstruct the new template/string literal replacement
@@ -201,20 +301,21 @@ async function transformWithOxc(
magicString.overwrite(node.start, node.end, replacement);
}
- },
+ }
});
- visitor.visit(program);
-
const outputCode = magicString.toString();
let outputMap;
if (map && magicString.hasChanged()) {
- const rawMap = magicString.generateMap({
+ // 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: options.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..071097315e5b 100644
--- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
+++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts
@@ -9,6 +9,7 @@
import assert from 'node:assert';
import { createHash } from 'node:crypto';
import { extname, join } from 'node:path';
+import { serialize } from 'node:v8';
import { WorkerPool } from '../../utils/worker-pool';
import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files';
import { type PersistentCacheStore, createPersistentCacheStore } from './cache';
@@ -19,6 +20,20 @@ import { type PersistentCacheStore, createPersistentCacheStore } from './cache';
*/
const LOCALIZE_KEYWORD = '$localize';
+/**
+ * Serializes the translation messages for a locale for transfer to an inliner Worker.
+ *
+ * A Blob is used because cloning one shares its data by reference, whereas sending the messages
+ * themselves copies them into a Worker for every request that carries them. A locale can contain
+ * tens of thousands of messages, which makes that copy the dominant cost of inlining a locale.
+ *
+ * @param translation The translation messages for a locale, if the locale has any.
+ * @returns A Blob containing the serialized messages, or undefined if the locale has none.
+ */
+function serializeTranslation(translation: Record | undefined): Blob | undefined {
+ return translation && new Blob([serialize(translation)]);
+}
+
/**
* Inlining options that should apply to all transformed code.
*/
@@ -125,6 +140,10 @@ export class I18nInliner {
await this.initCache();
const { shouldOptimize, missingTranslation } = this.options;
+
+ // Serialized once here and then shared by the request for every file of this locale
+ const translationBlob = serializeTranslation(translation);
+
// Request inlining for each file that contains localize calls
const requests = [];
@@ -138,10 +157,12 @@ export class I18nInliner {
let cacheResultPromise = Promise.resolve(null);
if (this.#cache) {
- fileCacheKeyBase ??= Buffer.from(
- JSON.stringify({ locale, translation, missingTranslation, shouldOptimize }),
- 'utf-8',
- );
+ // The options are digested here so that each file's key is derived from a fixed number
+ // of bytes. Hashing the options directly would re-hash the full set of messages, which
+ // can be several megabytes, once for every file.
+ fileCacheKeyBase ??= createHash('sha256')
+ .update(JSON.stringify({ locale, translation, missingTranslation, shouldOptimize }))
+ .digest();
// NOTE: If additional options are added, this may need to be updated.
// TODO: Consider xxhash or similar instead of SHA256
@@ -160,7 +181,11 @@ export class I18nInliner {
return cachedResult;
}
- const result = await this.#workerPool.run({ filename, locale, translation });
+ const result = await this.#workerPool.run({
+ filename,
+ locale,
+ translation: translationBlob,
+ });
if (this.#cache && cacheKey) {
try {
// Failure to set the value should not fail the transform
@@ -227,7 +252,12 @@ export class I18nInliner {
}
const { output, messages } = await this.#workerPool.run(
- { code: templateCode, filename: templateId, locale, translation },
+ {
+ code: templateCode,
+ filename: templateId,
+ locale,
+ translation: serializeTranslation(translation),
+ },
{ name: 'inlineCode' },
);
@@ -252,8 +282,8 @@ export class I18nInliner {
* Stops all active transformation tasks and shuts down all workers.
* @returns A void promise that resolves when closing is complete.
*/
- close(): Promise {
- return this.#workerPool.destroy();
+ async close(): Promise {
+ await Promise.allSettled([this.#cache?.close(), this.#workerPool.destroy()]);
}
/**
diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts
new file mode 100644
index 000000000000..e89acc41c161
--- /dev/null
+++ b/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts
@@ -0,0 +1,298 @@
+/**
+ * @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 'esbuild';
+import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files';
+import { I18nInliner } from './i18n-inliner';
+
+/**
+ * A module that uses a `$localize` message with an explicit message identifier so that the
+ * translations for a test can be keyed by a known name.
+ */
+const GREETING_SOURCE = 'export const greeting = $localize`:@@greeting:Hello`;\n';
+
+/**
+ * Creates the parsed translation form that `@angular/localize` expects for a message without
+ * placeholders.
+ */
+function translationFor(message: string): Record {
+ return { messageParts: [message], placeholderNames: [], text: message };
+}
+
+function browserFile(path: string, contents: string): BuildOutputFile {
+ return createOutputFile(path, contents, BuildOutputFileType.Browser);
+}
+
+function findFile(outputFiles: BuildOutputFile[], path: string): BuildOutputFile {
+ const file = outputFiles.find((output) => output.path === path);
+ if (!file) {
+ throw new Error(`Expected output files to contain '${path}'.`);
+ }
+
+ return file;
+}
+
+describe('I18nInliner', () => {
+ let inliner: I18nInliner | undefined;
+
+ // A single thread is used throughout so that every file of every locale is inlined by the same
+ // Worker. Any translation state that a Worker retains between requests is then observable.
+ function createInliner(outputFiles: BuildOutputFile[]): I18nInliner {
+ inliner = new I18nInliner({ missingTranslation: 'warning', outputFiles }, 1);
+
+ return inliner;
+ }
+
+ afterEach(async () => {
+ await inliner?.close();
+ inliner = undefined;
+ });
+
+ it('inlines the translations of a locale', async () => {
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', GREETING_SOURCE),
+ ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') });
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(findFile(outputFiles, 'main.js').text).toContain('"Bonjour"');
+ expect(findFile(outputFiles, 'main.js').text).not.toContain('$localize');
+ });
+
+ it('inlines the translations of each locale when several are inlined in sequence', async () => {
+ const localeInliner = createInliner([browserFile('main.js', GREETING_SOURCE)]);
+
+ const french = await localeInliner.inlineForLocale('fr', {
+ greeting: translationFor('Bonjour'),
+ });
+ const german = await localeInliner.inlineForLocale('de', { greeting: translationFor('Hallo') });
+ // Repeats the first locale to cover a locale being inlined again after another has been.
+ const frenchAgain = await localeInliner.inlineForLocale('fr', {
+ greeting: translationFor('Bonjour'),
+ });
+
+ expect(findFile(french.outputFiles, 'main.js').text).toContain('"Bonjour"');
+ expect(findFile(german.outputFiles, 'main.js').text).toContain('"Hallo"');
+ expect(findFile(frenchAgain.outputFiles, 'main.js').text).toContain('"Bonjour"');
+ });
+
+ it('inlines the translations of a locale into every file that uses them', async () => {
+ const { outputFiles } = await createInliner([
+ browserFile('main.js', GREETING_SOURCE),
+ browserFile('chunk.js', GREETING_SOURCE),
+ ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') });
+
+ expect(findFile(outputFiles, 'main.js').text).toContain('"Bonjour"');
+ expect(findFile(outputFiles, 'chunk.js').text).toContain('"Bonjour"');
+ });
+
+ it('retains the original messages for a locale without translations', async () => {
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', GREETING_SOURCE),
+ ]).inlineForLocale('en-US', undefined);
+
+ // A locale without translations is the source locale, so its messages are not missing.
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(findFile(outputFiles, 'main.js').text).toContain('"Hello"');
+ });
+
+ it('warns and retains the original message when a locale is missing a translation', async () => {
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', GREETING_SOURCE),
+ ]).inlineForLocale('fr', { unrelated: translationFor('Sans rapport') });
+
+ expect(errors).toEqual([]);
+ expect(warnings.length).toBe(1);
+ expect(warnings[0]).toContain('greeting');
+ expect(findFile(outputFiles, 'main.js').text).toContain('"Hello"');
+ });
+
+ it('replaces the locale placeholder with the locale being inlined', async () => {
+ // The placeholder is only inlined for files that use `$localize`, which is where the build
+ // inserts it, so the message is present alongside it here.
+ const { outputFiles } = await createInliner([
+ browserFile('main.js', `export const locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`),
+ ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') });
+
+ expect(findFile(outputFiles, 'main.js').text).toContain('"fr"');
+ expect(findFile(outputFiles, 'main.js').text).not.toContain('___NG_LOCALE_INSERT___');
+ });
+
+ it('remaps the source map of a file it modifies', async () => {
+ // esbuild provides a map from the emitted code back to an original file, matching what the
+ // inliner receives during a build.
+ const { code, map } = await transform(GREETING_SOURCE, {
+ sourcefile: 'greeting.ts',
+ loader: 'ts',
+ sourcemap: 'external',
+ });
+
+ const { outputFiles } = await createInliner([
+ browserFile('main.js', code),
+ browserFile('main.js.map', map),
+ ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') });
+
+ const outputMap = JSON.parse(findFile(outputFiles, 'main.js.map').text) as {
+ version: number;
+ sources: string[];
+ mappings: string;
+ };
+
+ expect(outputMap.version).toBe(3);
+ // The map must still resolve to the original file rather than to the inliner's input.
+ expect(outputMap.sources).toContain('greeting.ts');
+ expect(outputMap.mappings.length).toBeGreaterThan(0);
+ });
+
+ describe('inlineTemplateUpdate', () => {
+ it('inlines the translations of a locale into a template update', async () => {
+ const { code, errors, warnings } = await createInliner([]).inlineTemplateUpdate(
+ 'fr',
+ { greeting: translationFor('Bonjour') },
+ GREETING_SOURCE,
+ 'template-id',
+ );
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(code).toContain('"Bonjour"');
+ expect(code).not.toContain('$localize');
+ });
+
+ it('retains the original messages for a locale without translations', async () => {
+ const { code, errors, warnings } = await createInliner([]).inlineTemplateUpdate(
+ 'en-US',
+ undefined,
+ GREETING_SOURCE,
+ 'template-id',
+ );
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(code).toContain('"Hello"');
+ });
+
+ it('returns the code untouched when it has no localize calls', async () => {
+ const source = 'export const answer = 42;\n';
+ const { code } = await createInliner([]).inlineTemplateUpdate(
+ 'fr',
+ { greeting: translationFor('Bonjour') },
+ source,
+ 'template-id',
+ );
+
+ expect(code).toBe(source);
+ });
+ });
+
+ it('safely inlines translations containing special characters, quotes, and newlines', async () => {
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', GREETING_SOURCE),
+ ]).inlineForLocale('fr', {
+ greeting: translationFor('Bonjour "mon ami" \\ \' \n '),
+ });
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(findFile(outputFiles, 'main.js').text).toBe(
+ 'export const greeting = "Bonjour \\"mon ami\\" \\\\ \' \\n ";\n',
+ );
+ });
+
+ it('inlines translations containing placeholders', async () => {
+ const source = 'export const welcome = (name) => $localize`:@@welcome:Hello ${name}!`;\n';
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', source),
+ ]).inlineForLocale('fr', {
+ welcome: {
+ messageParts: ['Bonjour ', ' !'],
+ placeholderNames: ['PH'],
+ text: 'Bonjour {$PH} !',
+ },
+ });
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(findFile(outputFiles, 'main.js').text).toBe(
+ 'export const welcome = (name) => `Bonjour ${name} !`;\n',
+ );
+ });
+
+ it('inlines multiple localize calls within the same file', async () => {
+ const source =
+ 'export const a = $localize`:@@greeting:Hello`;\nexport const b = $localize`:@@farewell:Goodbye`;\n';
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', source),
+ ]).inlineForLocale('fr', {
+ greeting: translationFor('Bonjour'),
+ farewell: translationFor('Au revoir'),
+ });
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(findFile(outputFiles, 'main.js').text).toBe(
+ 'export const a = "Bonjour";\nexport const b = "Au revoir";\n',
+ );
+ });
+
+ it('inlines translations across multiple files using multiple worker threads in parallel', async () => {
+ inliner = new I18nInliner(
+ {
+ missingTranslation: 'warning',
+ outputFiles: [
+ browserFile('main.js', GREETING_SOURCE),
+ browserFile('chunk1.js', GREETING_SOURCE),
+ browserFile('chunk2.js', GREETING_SOURCE),
+ browserFile('chunk3.js', GREETING_SOURCE),
+ ],
+ },
+ 4,
+ );
+
+ const { outputFiles, errors, warnings } = await inliner.inlineForLocale('fr', {
+ greeting: translationFor('Bonjour'),
+ });
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ for (const name of ['main.js', 'chunk1.js', 'chunk2.js', 'chunk3.js']) {
+ expect(findFile(outputFiles, name).text).toBe('export const greeting = "Bonjour";\n');
+ }
+ });
+
+ it('leaves files without localize calls unmodified', async () => {
+ const { outputFiles } = await createInliner([
+ browserFile('main.js', GREETING_SOURCE),
+ browserFile('other.js', 'export const answer = 42;\n'),
+ ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') });
+
+ expect(findFile(outputFiles, 'other.js').text).toBe('export const answer = 42;\n');
+ });
+
+ it('inlines nested $localize calls in post-order', async () => {
+ const source =
+ 'export const msg = $localize`:@@outer:You selected ${$localize`:@@inner:Apple`} for delivery.`;\n';
+ const { outputFiles, errors, warnings } = await createInliner([
+ browserFile('main.js', source),
+ ]).inlineForLocale('fr', {
+ inner: translationFor('Pomme'),
+ outer: {
+ messageParts: ['Vous avez sélectionné ', ' pour la livraison.'],
+ placeholderNames: ['PH'],
+ text: 'Vous avez sélectionné {$PH} pour la livraison.',
+ },
+ });
+
+ expect(errors).toEqual([]);
+ expect(warnings).toEqual([]);
+ expect(findFile(outputFiles, 'main.js').text).toBe(
+ 'export const msg = `Vous avez sélectionné ${"Pomme"} pour la livraison.`;\n',
+ );
+ });
+});
diff --git a/packages/angular/build/src/tools/esbuild/license-extractor.ts b/packages/angular/build/src/tools/esbuild/license-extractor.ts
index 6629bbc387bf..890ebdd9826f 100644
--- a/packages/angular/build/src/tools/esbuild/license-extractor.ts
+++ b/packages/angular/build/src/tools/esbuild/license-extractor.ts
@@ -7,7 +7,7 @@
*/
import type { Metafile } from 'esbuild';
-import { readFile } from 'node:fs/promises';
+import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
/**
@@ -30,9 +30,9 @@ const NODE_MODULE_SEGMENT = 'node_modules';
const CUSTOM_LICENSE_TEXT = 'SEE LICENSE IN ';
/**
- * A list of commonly named license files found within packages.
+ * A regular expression for commonly named license files found within packages.
*/
-const LICENSE_FILES = ['LICENSE', 'LICENSE.txt', 'LICENSE.md'];
+const LICENSE_FILE_REGEXP = /^(?:mit-)?licen[cs]e(?:$|[-._])/i;
/**
* Header text that will be added to the top of the output license extraction file.
@@ -64,6 +64,7 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
let extractedLicenseContent = `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}`;
const seenPaths = new Set();
+ const seenPackageDirectories = new Set();
const seenPackages = new Set();
for (const entry of Object.values(metafile.outputs)) {
@@ -110,6 +111,11 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
: nameOrScope;
const packageDirectory = path.join(baseDirectory, packageName);
+ if (seenPackageDirectories.has(packageDirectory)) {
+ continue;
+ }
+ seenPackageDirectories.add(packageDirectory);
+
// Load the package's metadata to find the package's name, version, and license type
const packageJsonPath = path.join(packageDirectory, 'package.json');
let packageJson;
@@ -136,12 +142,12 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
let licenseText = '';
if (
typeof packageJson.license === 'string' &&
- packageJson.license.toLowerCase().startsWith(CUSTOM_LICENSE_TEXT)
+ packageJson.license.toUpperCase().startsWith(CUSTOM_LICENSE_TEXT)
) {
// Attempt to load the package's custom license
let customLicensePath;
const customLicenseFile = path.normalize(
- packageJson.license.slice(CUSTOM_LICENSE_TEXT.length + 1).trim(),
+ packageJson.license.slice(CUSTOM_LICENSE_TEXT.length).trim(),
);
if (customLicenseFile.startsWith('..') || path.isAbsolute(customLicenseFile)) {
// Path is attempting to access files outside of the package
@@ -150,17 +156,20 @@ export async function extractLicenses(metafile: Metafile, rootDirectory: string)
customLicensePath = path.join(packageDirectory, customLicenseFile);
try {
licenseText = await readFile(customLicensePath, 'utf-8');
- break;
} catch {}
}
} else {
// Search for a license file within the root of the package
- for (const potentialLicense of LICENSE_FILES) {
- const packageLicensePath = path.join(packageDirectory, potentialLicense);
- try {
- licenseText = await readFile(packageLicensePath, 'utf-8');
- break;
- } catch {}
+ const entries = await readdir(packageDirectory, { withFileTypes: true }).catch(() => []);
+
+ for (const entry of entries) {
+ if ((entry.isFile() || entry.isSymbolicLink()) && LICENSE_FILE_REGEXP.test(entry.name)) {
+ const packageLicensePath = path.join(packageDirectory, entry.name);
+ try {
+ licenseText = await readFile(packageLicensePath, 'utf-8');
+ break;
+ } catch {}
+ }
}
}
diff --git a/packages/angular/build/src/tools/esbuild/load-result-cache.ts b/packages/angular/build/src/tools/esbuild/load-result-cache.ts
index 30067486a384..40936c6a87d1 100644
--- a/packages/angular/build/src/tools/esbuild/load-result-cache.ts
+++ b/packages/angular/build/src/tools/esbuild/load-result-cache.ts
@@ -48,12 +48,26 @@ export function createCachedLoad(
export class MemoryLoadResultCache implements LoadResultCache {
#loadResults = new Map();
#fileDependencies = new Map>();
+ #watchFilesPerKey = new Map>();
get(path: string): OnLoadResult | undefined {
return this.#loadResults.get(path);
}
async put(path: string, result: OnLoadResult): Promise {
+ if (result.errors && result.errors.length > 0) {
+ const previousWatchFiles = this.#watchFilesPerKey.get(path);
+ if (previousWatchFiles) {
+ result.watchFiles = Array.from(
+ new Set([...(result.watchFiles ?? []), ...previousWatchFiles]),
+ );
+ }
+ } else if (result.watchFiles && result.watchFiles.length > 0) {
+ this.#watchFilesPerKey.set(path, [...result.watchFiles]);
+ } else {
+ this.#watchFilesPerKey.delete(path);
+ }
+
this.#loadResults.set(path, result);
if (result.watchFiles) {
for (const watchFile of result.watchFiles) {
@@ -90,4 +104,10 @@ export class MemoryLoadResultCache implements LoadResultCache {
// are namespaced request paths and not disk-based file paths.
return [...this.#fileDependencies.keys()];
}
+
+ clear(): void {
+ this.#loadResults.clear();
+ this.#fileDependencies.clear();
+ this.#watchFilesPerKey.clear();
+ }
}
diff --git a/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts b/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts
new file mode 100644
index 000000000000..fb0396aea829
--- /dev/null
+++ b/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts
@@ -0,0 +1,85 @@
+/**
+ * @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 { MemoryLoadResultCache } from './load-result-cache';
+
+describe('MemoryLoadResultCache', () => {
+ let cache: MemoryLoadResultCache;
+
+ beforeEach(() => {
+ cache = new MemoryLoadResultCache();
+ });
+
+ it('should store and retrieve results', async () => {
+ const result = {
+ contents: 'body { color: red; }',
+ loader: 'css' as const,
+ };
+
+ await cache.put('file:/test/styles.css', result);
+ const cached = cache.get('file:/test/styles.css');
+
+ expect(cached).toBe(result);
+ });
+
+ it('should track watch files in fileDependencies', async () => {
+ const result = {
+ contents: 'body { color: red; }',
+ loader: 'css' as const,
+ watchFiles: ['/test/styles.css', '/test/theme.json'],
+ };
+
+ await cache.put('file:/test/styles.css', result);
+
+ expect(cache.watchFiles).toContain('/test/styles.css');
+ expect(cache.watchFiles).toContain('/test/theme.json');
+ });
+
+ it('should invalidate cached results when a dependency changes', async () => {
+ const result = {
+ contents: 'body { color: red; }',
+ loader: 'css' as const,
+ watchFiles: ['/test/styles.css', '/test/theme.json'],
+ };
+
+ await cache.put('file:/test/styles.css', result);
+ expect(cache.get('file:/test/styles.css')).toBe(result);
+
+ const invalidated = cache.invalidate('/test/theme.json');
+ expect(invalidated).toBeTrue();
+ expect(cache.get('file:/test/styles.css')).toBeUndefined();
+ });
+
+ it('should preserve previous watch files when caching an error result', async () => {
+ const successResult = {
+ contents: 'body { color: red; }',
+ loader: 'css' as const,
+ watchFiles: ['/test/styles.css', '/test/theme.json'],
+ };
+
+ await cache.put('file:/test/styles.css', successResult);
+ cache.invalidate('/test/theme.json');
+
+ // Simulate an incremental rebuild error result that only has the entry file in watchFiles
+ const errorResult = {
+ errors: [{ text: 'Syntax error in theme.json' }],
+ watchFiles: ['/test/styles.css'],
+ };
+
+ await cache.put('file:/test/styles.css', errorResult);
+
+ // Both the entry file and the previous dependency should be tracked
+ expect(cache.watchFiles).toContain('/test/styles.css');
+ expect(cache.watchFiles).toContain('/test/theme.json');
+
+ // Invalidating the dependency should clear the cached error result
+ const invalidated = cache.invalidate('/test/theme.json');
+ expect(invalidated).toBeTrue();
+ expect(cache.get('file:/test/styles.css')).toBeUndefined();
+ });
+});
diff --git a/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts b/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts
index 3d594668d44f..4f50515749df 100644
--- a/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts
+++ b/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts
@@ -15,6 +15,8 @@ export class SqliteCacheStore implements PersistentCacheStore {
#hasStmt: StatementSync | undefined;
#setStmt: StatementSync | undefined;
#updateAccessedStmt: StatementSync | undefined;
+ readonly #pendingAccessedKeys = new Set();
+ #flushTimeout: NodeJS.Timeout | undefined;
constructor(
readonly cachePath: string,
@@ -29,6 +31,9 @@ export class SqliteCacheStore implements PersistentCacheStore {
this.#db.exec('PRAGMA auto_vacuum = FULL;');
this.#db.exec('PRAGMA journal_mode = WAL;');
this.#db.exec('PRAGMA synchronous = NORMAL;');
+ this.#db.exec('PRAGMA busy_timeout = 5000;');
+ this.#db.exec('PRAGMA temp_store = MEMORY;');
+ this.#db.exec('PRAGMA mmap_size = 268435456;');
this.#db.exec(
'CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, last_accessed INTEGER NOT NULL) WITHOUT ROWID;',
);
@@ -46,13 +51,51 @@ export class SqliteCacheStore implements PersistentCacheStore {
return this.#db;
}
+ #queueAccessUpdate(key: string): void {
+ this.#pendingAccessedKeys.add(key);
+
+ if (this.#pendingAccessedKeys.size >= 100) {
+ this.#flushAccessUpdates();
+ } else if (!this.#flushTimeout) {
+ this.#flushTimeout = setTimeout(() => this.#flushAccessUpdates(), 500);
+ this.#flushTimeout.unref?.();
+ }
+ }
+
+ #flushAccessUpdates(): void {
+ if (this.#flushTimeout) {
+ clearTimeout(this.#flushTimeout);
+ this.#flushTimeout = undefined;
+ }
+
+ if (!this.#db || this.#pendingAccessedKeys.size === 0 || !this.#updateAccessedStmt) {
+ return;
+ }
+
+ try {
+ this.#db.exec('BEGIN IMMEDIATE TRANSACTION;');
+ for (const key of this.#pendingAccessedKeys) {
+ this.#updateAccessedStmt.run(key);
+ }
+ this.#db.exec('COMMIT;');
+ } catch {
+ try {
+ this.#db.exec('ROLLBACK;');
+ } catch {
+ // Ignore rollback errors if transaction was not active
+ }
+ } finally {
+ this.#pendingAccessedKeys.clear();
+ }
+ }
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async get(key: string): Promise {
this.#ensureDb();
const row = this.#getStmt?.get(key) as { value: string } | undefined;
if (row) {
- this.#updateAccessedStmt?.run(key);
+ this.#queueAccessUpdate(key);
try {
return JSON.parse(row.value);
@@ -72,6 +115,7 @@ export class SqliteCacheStore implements PersistentCacheStore {
async set(key: string, value: unknown): Promise {
this.#ensureDb();
+ this.#pendingAccessedKeys.delete(key);
this.#setStmt?.run(key, JSON.stringify(value));
return this;
@@ -84,6 +128,9 @@ export class SqliteCacheStore implements PersistentCacheStore {
close(): void {
if (this.#db) {
try {
+ // Flush any pending access updates in one transaction before pruning
+ this.#flushAccessUpdates();
+
// 1. Delete items older than N days
this.#db
.prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);")
@@ -103,6 +150,12 @@ export class SqliteCacheStore implements PersistentCacheStore {
} catch {
// Pruning errors should not block build success
} finally {
+ if (this.#flushTimeout) {
+ clearTimeout(this.#flushTimeout);
+ this.#flushTimeout = undefined;
+ }
+ this.#pendingAccessedKeys.clear();
+
this.#getStmt = undefined;
this.#hasStmt = undefined;
this.#setStmt = undefined;
diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts
index 78925f35835e..2c40e007350e 100644
--- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts
+++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts
@@ -427,6 +427,7 @@ async function compileString(
},
},
],
+ watchFiles: error.file && error.file !== filename ? [filename, error.file] : [filename],
};
} else {
assertIsError(error);
diff --git a/packages/angular/build/src/tools/esbuild/target.ts b/packages/angular/build/src/tools/esbuild/target.ts
new file mode 100644
index 000000000000..2925905276b7
--- /dev/null
+++ b/packages/angular/build/src/tools/esbuild/target.ts
@@ -0,0 +1,116 @@
+/**
+ * @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 { coerce, compare, minVersion } from 'semver';
+
+/**
+ * Compares two target version strings.
+ *
+ * This function is used to determine the lowest version for a given browser target.
+ *
+ * @param a The first version string.
+ * @param b The second version string.
+ * @returns A negative value if `a` is lower than `b`, a positive value if `a` is higher than `b`, and 0 if they are equal.
+ */
+function compareTargetVersions(a: string, b: string): number {
+ const aVersion = coerce(a);
+ const bVersion = coerce(b);
+
+ if (!aVersion || !bVersion) {
+ return aVersion ? -1 : bVersion ? 1 : 0;
+ }
+
+ return compare(aVersion, bVersion);
+}
+
+// https://esbuild.github.io/api/#target
+const ESBUILD_SUPPORTED_BROWSERS: ReadonlySet = new Set([
+ 'chrome',
+ 'edge',
+ 'firefox',
+ 'ie',
+ 'ios',
+ 'node',
+ 'opera',
+ 'safari',
+]);
+
+/**
+ * Transform browserlists result to esbuild target.
+ *
+ * Only the lowest version for each browser is returned to avoid issues with esbuild and rolldown
+ * when multiple versions of the same target engine are specified.
+ *
+ * @see https://esbuild.github.io/api/#target
+ * @see https://github.com/evanw/esbuild/issues/4509
+ * @see https://github.com/rolldown/rolldown/issues/10633
+ */
+export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] {
+ const browsers = new Map();
+
+ for (const browser of supportedBrowsers) {
+ let [browserName, version] = browser.toLowerCase().split(' ');
+ if (!browserName || !version) {
+ continue;
+ }
+
+ // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios`
+ if (browserName === 'ios_saf') {
+ browserName = 'ios';
+ }
+
+ if (!ESBUILD_SUPPORTED_BROWSERS.has(browserName)) {
+ continue;
+ }
+
+ // browserslist uses ranges `15.2-15.3` versions but only the lowest is required
+ // to perform minimum supported feature checks. esbuild also expects a single version.
+ [version] = version.split('-');
+
+ if (browserName === 'safari' && version === 'tp') {
+ // esbuild only supports numeric versions so `TP` is converted to a high number (999) since
+ // a Technology Preview (TP) of Safari is assumed to support all currently known features.
+ version = '999';
+ } else if (!version.includes('.')) {
+ // A lone major version is considered by esbuild to include all minor versions. However,
+ // browserslist does not and is also inconsistent in its `.0` version naming. For example,
+ // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`.
+ version += '.0';
+ }
+
+ const current = browsers.get(browserName);
+ if (!current || compareTargetVersions(version, current) < 0) {
+ browsers.set(browserName, version);
+ }
+ }
+
+ return Array.from(browsers, ([browserName, version]) => browserName + version);
+}
+
+const SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE';
+
+/**
+ * Transform supported Node.js versions to esbuild target.
+ *
+ * Only the lowest Node.js version is returned to avoid issues with esbuild and rolldown
+ * when multiple versions of the same target engine are specified.
+ *
+ * @see https://esbuild.github.io/api/#target
+ * @see https://github.com/evanw/esbuild/issues/4509
+ * @see https://github.com/rolldown/rolldown/issues/10633
+ */
+export function getSupportedNodeTargets(): string[] {
+ if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') {
+ // Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions.
+ return [];
+ }
+
+ const parsed = minVersion(SUPPORTED_NODE_VERSIONS);
+
+ return parsed ? ['node' + parsed.version] : [];
+}
diff --git a/packages/angular/build/src/tools/esbuild/target_spec.ts b/packages/angular/build/src/tools/esbuild/target_spec.ts
new file mode 100644
index 000000000000..e5a375a1412b
--- /dev/null
+++ b/packages/angular/build/src/tools/esbuild/target_spec.ts
@@ -0,0 +1,80 @@
+/**
+ * @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 { getSupportedNodeTargets, transformSupportedBrowsersToTargets } from './target';
+
+describe('esbuild target', () => {
+ describe('transformSupportedBrowsersToTargets', () => {
+ it('should return the smallest version for each browser', () => {
+ const targets = transformSupportedBrowsersToTargets([
+ 'chrome 122',
+ 'chrome 120',
+ 'chrome 121',
+ 'firefox 116',
+ 'firefox 115',
+ 'safari 17.0',
+ 'safari 16.4',
+ ]);
+
+ expect(targets).toEqual(['chrome120.0', 'firefox115.0', 'safari16.4']);
+ });
+
+ it('should handle version ranges and pick the lowest version', () => {
+ const targets = transformSupportedBrowsersToTargets([
+ 'ios_saf 15.4',
+ 'ios_saf 15.2-15.3',
+ 'ios_saf 16.0',
+ ]);
+
+ expect(targets).toEqual(['ios15.2']);
+ });
+
+ it('should handle Safari TP (Technology Preview)', () => {
+ const targetsWithOlderSafari = transformSupportedBrowsersToTargets([
+ 'safari TP',
+ 'safari 16.4',
+ ]);
+ expect(targetsWithOlderSafari).toEqual(['safari16.4']);
+
+ const targetsWithOnlyTP = transformSupportedBrowsersToTargets(['safari TP']);
+ expect(targetsWithOnlyTP).toEqual(['safari999']);
+ });
+
+ it('should ignore browsers not supported by esbuild', () => {
+ const targets = transformSupportedBrowsersToTargets([
+ 'android 4.4',
+ 'samsung 22',
+ 'kaios 2.5',
+ 'chrome 115',
+ ]);
+
+ expect(targets).toEqual(['chrome115.0']);
+ });
+
+ it('should return empty array for empty supportedBrowsers', () => {
+ const targets = transformSupportedBrowsersToTargets([]);
+ expect(targets).toEqual([]);
+ });
+
+ it('should handle malformed or incomplete browser strings gracefully', () => {
+ const targets = transformSupportedBrowsersToTargets(['chrome', 'firefox ', '']);
+ expect(targets).toEqual([]);
+ });
+
+ it('should handle single major versions by appending .0', () => {
+ const targets = transformSupportedBrowsersToTargets(['chrome 120', 'edge 120']);
+ expect(targets).toEqual(['chrome120.0', 'edge120.0']);
+ });
+ });
+
+ describe('getSupportedNodeTargets', () => {
+ it('should return empty array when node versions are not stamped', () => {
+ expect(getSupportedNodeTargets()).toEqual([]);
+ });
+ });
+});
diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts
index 6d76623f1493..e4881e184862 100644
--- a/packages/angular/build/src/tools/esbuild/utils.ts
+++ b/packages/angular/build/src/tools/esbuild/utils.ts
@@ -12,7 +12,6 @@ import { Listr } from 'listr2';
import { basename, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { brotliCompress } from 'node:zlib';
-import { coerce } from 'semver';
import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import { OutputMode } from '../../builders/application/schema';
import { BudgetCalculatorResult } from '../../utils/bundle-calculator';
@@ -213,7 +212,7 @@ export async function emitFilesToDisk(
writeFileCallback: (file: T) => Promise,
): Promise {
// Write files in groups of MAX_CONCURRENT_WRITES to avoid too many open files
- for (let fileIndex = 0; fileIndex < files.length; ) {
+ for (let fileIndex = 0; fileIndex < files.length;) {
const groupMax = Math.min(fileIndex + MAX_CONCURRENT_WRITES, files.length);
const actions = [];
@@ -225,71 +224,6 @@ export async function emitFilesToDisk(
}
}
-/**
- * Transform browserlists result to esbuild target.
- * @see https://esbuild.github.io/api/#target
- */
-export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] {
- const transformed: string[] = [];
-
- // https://esbuild.github.io/api/#target
- const esBuildSupportedBrowsers = new Set([
- 'chrome',
- 'edge',
- 'firefox',
- 'ie',
- 'ios',
- 'node',
- 'opera',
- 'safari',
- ]);
-
- for (const browser of supportedBrowsers) {
- let [browserName, version] = browser.toLowerCase().split(' ');
-
- // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios`
- if (browserName === 'ios_saf') {
- browserName = 'ios';
- }
-
- // browserslist uses ranges `15.2-15.3` versions but only the lowest is required
- // to perform minimum supported feature checks. esbuild also expects a single version.
- [version] = version.split('-');
-
- if (esBuildSupportedBrowsers.has(browserName)) {
- if (browserName === 'safari' && version === 'tp') {
- // esbuild only supports numeric versions so `TP` is converted to a high number (999) since
- // a Technology Preview (TP) of Safari is assumed to support all currently known features.
- version = '999';
- } else if (!version.includes('.')) {
- // A lone major version is considered by esbuild to include all minor versions. However,
- // browserslist does not and is also inconsistent in its `.0` version naming. For example,
- // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`.
- version += '.0';
- }
-
- transformed.push(browserName + version);
- }
- }
-
- return transformed;
-}
-
-const SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE';
-
-/**
- * Transform supported Node.js versions to esbuild target.
- * @see https://esbuild.github.io/api/#target
- */
-export function getSupportedNodeTargets(): string[] {
- if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') {
- // Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions.
- return [];
- }
-
- return SUPPORTED_NODE_VERSIONS.split('||').map((v) => 'node' + coerce(v)?.version);
-}
-
interface BuildManifest {
errors: string[];
warnings: string[];
diff --git a/packages/angular/build/src/tools/vite/utils.ts b/packages/angular/build/src/tools/vite/utils.ts
index f2b54d403334..dc7094b8ffe4 100644
--- a/packages/angular/build/src/tools/vite/utils.ts
+++ b/packages/angular/build/src/tools/vite/utils.ts
@@ -48,6 +48,7 @@ export type RolldownLoaderOption = Exclude<
>['moduleTypes'];
export function getDepOptimizationConfig({
+ target,
disabled,
exclude,
include,
@@ -56,6 +57,7 @@ export function getDepOptimizationConfig({
thirdPartySourcemaps,
define = {},
}: {
+ target: string[];
disabled: boolean;
exclude: string[];
include: string[];
@@ -73,6 +75,7 @@ export function getDepOptimizationConfig({
noDiscovery: disabled,
rolldownOptions: {
transform: {
+ target,
define,
},
moduleTypes: loader,
diff --git a/packages/angular/build/src/utils/bundle-calculator.ts b/packages/angular/build/src/utils/bundle-calculator.ts
index 3349a8a40830..71a75dafde02 100644
--- a/packages/angular/build/src/utils/bundle-calculator.ts
+++ b/packages/angular/build/src/utils/bundle-calculator.ts
@@ -152,6 +152,8 @@ function calculateSizes(budget: BudgetEntry, stats: BudgetStats): Size[] {
}
abstract class Calculator {
+ private assetMap?: ReadonlyMap;
+
constructor(
protected budget: BudgetEntry,
protected chunks: BudgetChunk[],
@@ -167,15 +169,24 @@ abstract class Calculator {
return 0;
}
+ if (!this.assetMap) {
+ const map = new Map();
+ for (const asset of this.assets) {
+ map.set(asset.name, asset.size);
+ }
+ this.assetMap = map;
+ }
+ const assetMap = this.assetMap;
+
return chunk.files
.filter((file) => !file.endsWith('.map'))
.map((file) => {
- const asset = this.assets.find((asset) => asset.name === file);
- if (!asset) {
+ const assetSize = assetMap.get(file);
+ if (assetSize === undefined) {
throw new Error(`Could not find asset for file: ${file}`);
}
- return asset.size;
+ return assetSize;
})
.reduce((l, r) => l + r, 0);
}
diff --git a/packages/angular/build/src/utils/index-file/auto-csp.ts b/packages/angular/build/src/utils/index-file/auto-csp.ts
index 0e1dfe3ed916..df33f0766607 100644
--- a/packages/angular/build/src/utils/index-file/auto-csp.ts
+++ b/packages/angular/build/src/utils/index-file/auto-csp.ts
@@ -22,6 +22,8 @@ interface SrcScriptTag {
type?: string;
async: boolean;
defer: boolean;
+ integrity?: string;
+ crossOrigin?: string;
}
/**
@@ -106,11 +108,14 @@ export async function autoCsp(html: string, unsafeEval = false): Promise
// If there are any interesting attributes, note them down.
const scriptType = getScriptAttributeValue(tag, 'type');
if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) {
+ const crossOrigin = getScriptAttributeValue(tag, 'crossorigin');
scriptContent.push({
src: src,
type: scriptType,
async: getScriptAttributeValue(tag, 'async') !== undefined,
defer: getScriptAttributeValue(tag, 'defer') !== undefined,
+ integrity: getScriptAttributeValue(tag, 'integrity'),
+ crossOrigin: crossOrigin === '' ? 'anonymous' : crossOrigin,
});
return; // Skip writing my script tag until we've read it all.
@@ -267,36 +272,52 @@ function createLoaderScript(srcList: SrcScriptTag[], enableTrustedTypes = false)
const srcAttr = encodeURI(s.src).replaceAll("'", "\\'");
// Can only be 'module' or a JS MIME type or an empty string.
const typeAttr = s.type ? "'" + s.type + "'" : "''";
- const asyncAttr = s.async ? 'true' : 'false';
- const deferAttr = s.defer ? 'true' : 'false';
+ const asyncAttr = !!s.async;
+ const deferAttr = !!s.defer;
+ const integrityAttr = JSON.stringify(s.integrity ?? null).replaceAll('<', '\\u003c');
+ const crossOriginAttr = JSON.stringify(s.crossOrigin ?? null).replaceAll('<', '\\u003c');
- return `['${srcAttr}', ${typeAttr}, ${asyncAttr}, ${deferAttr}]`;
+ return `['${srcAttr}', ${typeAttr}, ${asyncAttr}, ${deferAttr}, ${integrityAttr}, ${crossOriginAttr}]`;
})
.join();
return enableTrustedTypes
- ? `
- var scripts = [${srcListFormatted}];
- var policy = self.trustedTypes && self.trustedTypes.createPolicy ?
+ ? `(() => {
+ const scripts = [${srcListFormatted}];
+ const policy = self.trustedTypes && self.trustedTypes.createPolicy ?
self.trustedTypes.createPolicy('angular#auto-csp', {createScriptURL: function(u) {
- return scripts.includes(u) ? u : null;
+ return scripts.some(function(s) { return s[0] === u; }) ? u : null;
}}) : { createScriptURL: function(u) { return u; } };
scripts.forEach(function(scriptUrl) {
- var s = document.createElement('script');
+ const s = document.createElement('script');
s.src = policy.createScriptURL(scriptUrl[0]);
s.type = scriptUrl[1];
- s.async = !!scriptUrl[2];
- s.defer = !!scriptUrl[3];
+ s.async = scriptUrl[2];
+ s.defer = scriptUrl[3];
+ if (scriptUrl[4]) {
+ s.integrity = scriptUrl[4];
+ }
+ if (scriptUrl[5]) {
+ s.crossOrigin = scriptUrl[5];
+ }
document.lastElementChild.appendChild(s);
- });\n`
- : `
- var scripts = [${srcListFormatted}];
+ });
+})();\n`
+ : `(() => {
+ const scripts = [${srcListFormatted}];
scripts.forEach(function(scriptUrl) {
- var s = document.createElement('script');
+ const s = document.createElement('script');
s.src = scriptUrl[0];
s.type = scriptUrl[1];
- s.async = !!scriptUrl[2];
- s.defer = !!scriptUrl[3];
+ s.async = scriptUrl[2];
+ s.defer = scriptUrl[3];
+ if (scriptUrl[4]) {
+ s.integrity = scriptUrl[4];
+ }
+ if (scriptUrl[5]) {
+ s.crossOrigin = scriptUrl[5];
+ }
document.lastElementChild.appendChild(s);
- });\n`;
+ });
+})();\n`;
}
diff --git a/packages/angular/build/src/utils/index-file/auto-csp_spec.ts b/packages/angular/build/src/utils/index-file/auto-csp_spec.ts
index efbd338233ad..29e9bff68074 100644
--- a/packages/angular/build/src/utils/index-file/auto-csp_spec.ts
+++ b/packages/angular/build/src/utils/index-file/auto-csp_spec.ts
@@ -58,7 +58,7 @@ describe('auto-csp', () => {
const csps = getCsps(result);
expect(csps).toHaveSize(1);
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
- expect(result).toContain(`var scripts = [['./main.js', '', false, false]];`);
+ expect(result).toContain(`const scripts = [['./main.js', '', false, false, null, null]];`);
});
it('should rewrite a single source script in place', async () => {
@@ -78,7 +78,7 @@ describe('auto-csp', () => {
expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX);
// Our loader script appears after the HTML text content.
expect(result).toMatch(
- /Some text<\/div>\s*`);
@@ -166,12 +166,12 @@ describe('auto-csp', () => {
// Loader script for main.js and main2.js appear after 'foo' and before 'bar'.
expect(result).toMatch(
// eslint-disable-next-line max-len
- /console.log\('foo'\);<\/script>\s*
+ Some text
+