diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml index 30585fa707fd..1fcd15f0c5b9 100644 --- a/.github/workflows/assistant-to-the-branch-manager.yml +++ b/.github/workflows/assistant-to-the-branch-manager.yml @@ -18,6 +18,6 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: angular/dev-infra/github-actions/branch-manager@183403ae13b785698eaf13c819dda55b9fed430b # main + - uses: angular/dev-infra/github-actions/branch-manager@99d57027286c71dda92d8b91a3607abcf92d8bbd # main with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fad471f1244..cee6fbdfbda5 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -208,13 +208,13 @@ jobs: SAUCE_TUNNEL_IDENTIFIER: angular-cli-${{ github.workflow }}-${{ github.run_number }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Start Sauce Connect @@ -245,11 +245,11 @@ jobs: CIRCLE_BRANCH: ${{ github.ref_name }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - run: pnpm admin snapshots --verbose env: SNAPSHOT_BUILDS_GITHUB_TOKEN: ${{ secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN }} diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml index bb05d9b3dbfe..b12b3dc168ea 100644 --- a/.github/workflows/dev-infra.yml +++ b/.github/workflows/dev-infra.yml @@ -16,21 +16,21 @@ jobs: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/labeling/pull-request@183403ae13b785698eaf13c819dda55b9fed430b # main + - uses: angular/dev-infra/github-actions/labeling/pull-request@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + - uses: angular/dev-infra/github-actions/post-approval-changes@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + - uses: angular/dev-infra/github-actions/labeling/issue@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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 504f7e5136ad..b837b722a4f4 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -22,7 +22,7 @@ jobs: workflows: ${{ steps.workflows.outputs.workflows }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - id: workflows @@ -40,9 +40,9 @@ jobs: workflow: ${{ fromJSON(needs.list.outputs.workflows) }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile # We utilize the google-github-actions/auth action to allow us to get an active credential using workflow diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8b2baafbd3b4..c62816440333 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,9 +34,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/linting/licenses@99d57027286c71dda92d8b91a3607abcf92d8bbd # main build: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/setup@99d57027286c71dda92d8b91a3607abcf92d8bbd # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main + uses: angular/dev-infra/github-actions/bazel/configure-remote@99d57027286c71dda92d8b91a3607abcf92d8bbd # 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/CHANGELOG.md b/CHANGELOG.md index f7ae51f6dbe9..a67b3ed682d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + + +# 22.2.0-rc.0 (2026-09-16) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [6e7f809a7d](https://github.com/angular/angular-cli/commit/6e7f809a7d66e4b8d260a1c00297812412a926f1) | perf | skip eager yargs help message formatting during command execution | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [978351af71](https://github.com/angular/angular-cli/commit/978351af71889d4786a18fec9e193eb5b16af7dd) | fix | include inline component stylesheets in referenced watch files | +| [72dc9677cf](https://github.com/angular/angular-cli/commit/72dc9677cf743243d6cb22d2b81782022f71356e) | fix | pass load cache to compiler plugin and escape extension regex in polyfills | +| [31c045639e](https://github.com/angular/angular-cli/commit/31c045639e8bfd114cbd552d83cc70a968fe7f81) | fix | prevent stale bundler caching and correctly resolve load cache | +| [d97c8857c1](https://github.com/angular/angular-cli/commit/d97c8857c18da02fcc6c69a0ea652cc706c5c9b6) | perf | key Sass package resolutions without containing URL qualification | + + + # 22.2.0-next.7 (2026-09-10) diff --git a/MODULE.bazel b/MODULE.bazel index 470c5e258947..595813a5abf1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -10,30 +10,30 @@ bazel_dep(name = "rules_nodejs", version = "6.7.5") bazel_dep(name = "aspect_rules_js", version = "3.4.1") bazel_dep(name = "aspect_rules_ts", version = "3.10.1") 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 = "rules_cc", version = "0.2.25") +bazel_dep(name = "jq.bzl", version = "0.6.2") bazel_dep(name = "bazel_lib", version = "3.7.2") bazel_dep(name = "bazel_skylib", version = "1.9.2") -bazel_dep(name = "aspect_rules_esbuild", version = "0.27.0") +bazel_dep(name = "aspect_rules_esbuild", version = "0.27.1") bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4") bazel_dep(name = "rules_angular") git_override( module_name = "rules_angular", - commit = "5fa856469c642490a6e381a05d7f58f60fad1913", + commit = "7fdee07b9989b92b88f24c84471e01d6a0a9bc34", remote = "https://github.com/angular/rules_angular.git", ) bazel_dep(name = "devinfra") git_override( module_name = "devinfra", - commit = "183403ae13b785698eaf13c819dda55b9fed430b", + commit = "99d57027286c71dda92d8b91a3607abcf92d8bbd", remote = "https://github.com/angular/dev-infra.git", ) bazel_dep(name = "rules_browsers") git_override( module_name = "rules_browsers", - commit = "c37398d63f5e990d618d02f4c6fb2fefe40f1a79", + commit = "f87a5f7fc587219ff328da2972e06370f297a621", remote = "https://github.com/angular/rules_browsers.git", ) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 43e07b4b4f31..2f5985594341 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,6 +10,9 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", + "https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442", + "https://bcr.bazel.build/modules/apple_support/2.8.0/MODULE.bazel": "c45f5176057092afba0be6c48a9ea02101666802f8be536947e099e94757f8ed", + "https://bcr.bazel.build/modules/apple_support/2.8.0/source.json": "b7f6d87669c206adaa7d82cc44733349c63e341b80a0e049496ebd91d78c3472", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.0.0/MODULE.bazel": "e118477db5c49419a88d78ebc7a2c2cea9d49600fe0f490c1903324a2c16ecd9", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.2/MODULE.bazel": "30dfabbfae0139b1f0036e01c201dd4c0167da3017f0b7ef3820d78e07622989", @@ -18,8 +21,8 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", - "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.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_esbuild/0.27.1/MODULE.bazel": "99c3978959edd9892e4b513831b218a13cc84a0215bfe1b972a1e3a771c4670e", + "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.27.1/source.json": "20f515102cbcd0835d90bb3976b193eb58c2a0c534093c85cfd3bf6f06dba748", "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", @@ -40,6 +43,7 @@ "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", + "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", "https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e", @@ -85,8 +89,8 @@ "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", "https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f", "https://bcr.bazel.build/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2", - "https://bcr.bazel.build/modules/jq.bzl/0.6.1/MODULE.bazel": "f30c46e0a08a9f7566a8bf60a43d48abea960cd7f57b315b01e2762f1537eb52", - "https://bcr.bazel.build/modules/jq.bzl/0.6.1/source.json": "9ca9e2f90baa6a5bb0a49626ed9528554ec83165adf47b39792673ecc7feda22", + "https://bcr.bazel.build/modules/jq.bzl/0.6.2/MODULE.bazel": "e9c82f9b1e720d4ab0e232d32c05f0f4d0f92a8e8bb6a0da8f7cd27823b93e05", + "https://bcr.bazel.build/modules/jq.bzl/0.6.2/source.json": "e36f8ed173a6ca6e627f9d659ae504733d5a991859805661e4aacd34d8ae0639", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", @@ -100,6 +104,7 @@ "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", "https://bcr.bazel.build/modules/platforms/1.1.0/MODULE.bazel": "1c0c09f5bdcf4b3f924720d2478a3711cb39f4977019ca5988685e5b7e18b3d2", "https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344", @@ -131,8 +136,9 @@ "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", - "https://bcr.bazel.build/modules/rules_cc/0.2.22/MODULE.bazel": "94df4328edef9e44d38de5e73b037cd348e75e7ae55f4e21bf07878c41a31ebb", - "https://bcr.bazel.build/modules/rules_cc/0.2.22/source.json": "b2d6d6f9c332ce269ad75b89c6f3168d809a66173c9040210fb9bcc733ab42fa", + "https://bcr.bazel.build/modules/rules_cc/0.2.20/MODULE.bazel": "f5c07bce5ddcb99be21a0812ff5aadb439e688b7449c6542152363b2fd859c1a", + "https://bcr.bazel.build/modules/rules_cc/0.2.25/MODULE.bazel": "4a3d4f3606d3b3190be495dbc497acca552807d1d5540661ef0d1658472882e3", + "https://bcr.bazel.build/modules/rules_cc/0.2.25/source.json": "8df28a9a97b878fe45f0a4f4adf019869dda51a60ee2f6e15a0c79656a25058d", "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", @@ -203,8 +209,8 @@ "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/tar.bzl/0.10.4/MODULE.bazel": "e8f9ff79199e8d9eaad7f1b0a77ad74b30bb82d794b87d8ca942bead5de83ae9", - "https://bcr.bazel.build/modules/tar.bzl/0.10.8/MODULE.bazel": "443884cabe241f640cfef256b1de2ccb116752895532900a4be9500e1124323f", - "https://bcr.bazel.build/modules/tar.bzl/0.10.8/source.json": "4173be64b38e471d92d2eb139a6496311de6de75e710f04edce78c43122c5419", + "https://bcr.bazel.build/modules/tar.bzl/0.10.9/MODULE.bazel": "4636f5f6a7c34d3d0653634ed95a34560426d548c5301c6c5924c885ef737b46", + "https://bcr.bazel.build/modules/tar.bzl/0.10.9/source.json": "30a5cf0a8281adce53ab1c5a4291e88b2c161e2df1feb6cba4ee50556f1748d7", "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", @@ -222,8 +228,8 @@ "moduleExtensions": { "@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": { "general": { - "bzlTransitiveDigest": "ivnb7fuZRAghsUscNSqk/cyy9pTpUKwbi8xOKhgxNbo=", - "usagesDigest": "LZ71sshnfqI8R/HeWwBTwawxIa7KnitayBl2hYrqPo4=", + "bzlTransitiveDigest": "bn5qyJpbHTM21Kl9gvkARvwxKe1bH7JC951YjIa5vuk=", + "usagesDigest": "ZqBbSnzAi8ZyBL3PXnAGtDMIdC2pcuieJWMY3/94lvA=", "recordedInputs": [ "REPO_MAPPING:aspect_rules_esbuild+,aspect_rules_js aspect_rules_js+", "REPO_MAPPING:aspect_rules_esbuild+,aspect_tools_telemetry_report aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report", @@ -355,7 +361,7 @@ "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { "general": { "bzlTransitiveDigest": "zFgbaCUuPzH6bIE7CBHBVCqjWSJ78/WNXisdzXaKSnU=", - "usagesDigest": "2cLMn8ZcqY/6j/zH70TrPob+ENshTw9h48kUHxn1cUM=", + "usagesDigest": "B4C1UNcAOoRiMjmkXh3VFW7fY2YULy8OZA7RjfEM2UQ=", "recordedInputs": [ "REPO_MAPPING:aspect_tools_telemetry+,bazel_lib bazel_lib+", "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+", @@ -368,11 +374,11 @@ "deps": { "aspect_rules_js": "3.4.1", "aspect_rules_ts": "3.10.1", - "aspect_rules_esbuild": "0.27.0", + "aspect_rules_esbuild": "0.27.1", "aspect_rules_jasmine": "2.0.4", "aspect_tools_telemetry": "0.5.1" }, - "last_notice": 0 + "last_notice": 2 } } }, @@ -433,16 +439,16 @@ }, "@@rules_browsers+//browsers:extensions.bzl%browsers": { "general": { - "bzlTransitiveDigest": "eNfcpVF2YJFb8xQfN64Y9PQgMrXYK0ju3uglArt8IO8=", + "bzlTransitiveDigest": "jwGo+QiVY+i/AnJLUkEJ+vaplFYfygil2SJt8C7kM5o=", "usagesDigest": "FmXYJVoVJlnfUU8x8gObSvu4qWcco/9Faw61aC/wBF0=", "recordedInputs": [], "generatedRepoSpecs": { "rules_browsers_chrome_linux": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "ad2e73e1e42c0831930dfd02d4aa8653a5f23f60b0376a5d76ddf8ecd7a41e4b", + "sha256": "11d48f3e2ee11fc2a53a230eb364b2becc44c1b67a7aa4683edb7cb8a950b6ab", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/linux64/chrome-headless-shell-linux64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/linux64/chrome-headless-shell-linux64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-linux64/chrome-headless-shell" @@ -458,9 +464,9 @@ "rules_browsers_chrome_mac": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "d66b730ed9760ae88b83aca1fa89351ac027ede430be4f5283fabd156198762d", + "sha256": "42c60eb1793fe76aabf19664b03c299170e5de53baa23ae72578f063dc9b2614", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/mac-x64/chrome-headless-shell-mac-x64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/mac-x64/chrome-headless-shell-mac-x64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-x64/chrome-headless-shell" @@ -476,9 +482,9 @@ "rules_browsers_chrome_mac_arm": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "44c784c59af60ef62233b6100820ebb8b158e7d105b96c684818f007d6576848", + "sha256": "4afc1dbaeea03ad545b434c0f9eba50af326923fc265bc25927b3b4c7d9ca377", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/mac-arm64/chrome-headless-shell-mac-arm64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/mac-arm64/chrome-headless-shell-mac-arm64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-arm64/chrome-headless-shell" @@ -494,9 +500,9 @@ "rules_browsers_chrome_win64": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "996537a6c3d31bb5c75ef328af5d4827b5f92c4a36ff7ea250e437eadf46eac0", + "sha256": "9de8381d4ca9c9f0471fd1cce15e44146328c3e11f55fec43cd667ae1a5bd482", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/win64/chrome-headless-shell-win64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/win64/chrome-headless-shell-win64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-win64/chrome-headless-shell.exe" @@ -512,9 +518,9 @@ "rules_browsers_chromedriver_linux": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "f1db8caff52251dec46117a2f518a74cd1e4a2abadcbecc0d5bdd49e42175afa", + "sha256": "29be812bf4392619d06dfb5716d2bf9d72a278304670a594e3977d4c2cce7502", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/linux64/chromedriver-linux64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/linux64/chromedriver-linux64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-linux64/chromedriver" @@ -528,9 +534,9 @@ "rules_browsers_chromedriver_mac": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "5062675bc868f3f6a1e4850cab72b736a5c17f98f01d610736b8d0a6e009bf54", + "sha256": "d4dc141aadd9525e67c92fc78ec65e2926226c436eca7d90e39cdc813726513d", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/mac-x64/chromedriver-mac-x64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/mac-x64/chromedriver-mac-x64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-mac-x64/chromedriver" @@ -544,9 +550,9 @@ "rules_browsers_chromedriver_mac_arm": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "04f07a2a13e969f120dbea61eef291dc4e20617a6d424dec20e90f903da0263b", + "sha256": "984056c6d3dd189569cc89affd28564a9de949d58297279fc23a045c8fc53080", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/mac-arm64/chromedriver-mac-arm64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/mac-arm64/chromedriver-mac-arm64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-mac-arm64/chromedriver" @@ -560,9 +566,9 @@ "rules_browsers_chromedriver_win64": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "7701074890112ed3b780af26104cb9f57ce68e229ca3076bfade6a1af93730d7", + "sha256": "a5bcbd013b64e6a15acc84c164fe974d0f1a58d4e061fba79d24b31cced64d88", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/155.0.8043.0/win64/chromedriver-win64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/156.0.8060.2/win64/chromedriver-win64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-win64/chromedriver.exe" @@ -576,9 +582,9 @@ "rules_browsers_firefox_linux": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "fd9ec3f5f113d0825ca0bd1ab3c0756fbc40241034eefce6743be953bcca7473", + "sha256": "1d44cd02351c307c3e19061ea2a4d18a30f236e6be862b94f2282564afdb0167", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/155.0/linux-x86_64/en-US/firefox-155.0.tar.xz" + "https://archive.mozilla.org/pub/firefox/releases/156.0/linux-x86_64/en-US/firefox-156.0.tar.xz" ], "named_files": { "FIREFOX": "firefox/firefox" @@ -592,9 +598,9 @@ "rules_browsers_firefox_mac": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "3ae135f2023cf0c6cbce3bb757e06564ff8148b1266f682b8eccff0993a1a9f5", + "sha256": "fecc46103039ca4a77ecb2b2ef7b03892f922e13db0782f8570c512b2e458515", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/155.0/mac/en-US/Firefox%20155.0.dmg" + "https://archive.mozilla.org/pub/firefox/releases/156.0/mac/en-US/Firefox%20156.0.dmg" ], "named_files": { "FIREFOX": "Firefox.app/Contents/MacOS/firefox" @@ -608,9 +614,9 @@ "rules_browsers_firefox_mac_arm": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "3ae135f2023cf0c6cbce3bb757e06564ff8148b1266f682b8eccff0993a1a9f5", + "sha256": "fecc46103039ca4a77ecb2b2ef7b03892f922e13db0782f8570c512b2e458515", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/155.0/mac/en-US/Firefox%20155.0.dmg" + "https://archive.mozilla.org/pub/firefox/releases/156.0/mac/en-US/Firefox%20156.0.dmg" ], "named_files": { "FIREFOX": "Firefox.app/Contents/MacOS/firefox" @@ -624,9 +630,9 @@ "rules_browsers_firefox_win64": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "2718d4cc4b2cf089aa8ac370111652bc763838e1d2ed6d635d465fc17ab6327c", + "sha256": "86a027237f7408e8a4d18637c57d487455a704cbb2466a32d0d14c2207d2699a", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/155.0/win64/en-US/Firefox%20Setup%20155.0.exe" + "https://archive.mozilla.org/pub/firefox/releases/156.0/win64/en-US/Firefox%20Setup%20156.0.exe" ], "named_files": { "FIREFOX": "core/firefox.exe" diff --git a/modules/testing/builder/package.json b/modules/testing/builder/package.json index 3e0dc743f7ea..d76148352027 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": "30.0.1", - "ng-packagr": "22.2.0-next.5", + "ng-packagr": "22.2.0-rc.0", "rxjs": "7.8.2", "vitest": "5.0.1" } diff --git a/modules/testing/builder/src/builder-harness.ts b/modules/testing/builder/src/builder-harness.ts index 67b5f760d148..570939cecd7f 100644 --- a/modules/testing/builder/src/builder-harness.ts +++ b/modules/testing/builder/src/builder-harness.ts @@ -111,7 +111,7 @@ export class BuilderHarness { } } - private resolvePath(path: string): string { + resolvePath(path: string): string { return join(getSystemPath(this.host.root()), path); } diff --git a/package.json b/package.json index 69fd5f4f7f42..331ab53b141f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "22.2.0-next.7", + "version": "22.2.0-rc.0", "private": true, "description": "Software Development Kit for Angular", "keywords": [ @@ -15,6 +15,7 @@ "bazel": "bazelisk", "test": "bazel test //packages/...", "build": "pnpm --silent admin build", + "benchmark": "node --no-warnings=ExperimentalWarning --experimental-transform-types --expose-gc ./scripts/devkit-admin.mts benchmark", "build-schema": "bazel build //... --build_tag_filters schema --symlink_prefix dist-schema/", "lint": "eslint --cache --max-warnings=0", "templates": "pnpm --silent admin templates", @@ -42,23 +43,23 @@ }, "homepage": "https://github.com/angular/angular-cli", "dependencies": { - "@angular/compiler-cli": "22.2.0-next.5", + "@angular/compiler-cli": "22.2.0-rc.0", "typescript": "6.0.3" }, "devDependencies": { - "@angular/animations": "22.2.0-next.5", - "@angular/cdk": "22.2.0-next.4", - "@angular/common": "22.2.0-next.5", - "@angular/compiler": "22.2.0-next.5", - "@angular/core": "22.2.0-next.5", - "@angular/forms": "22.2.0-next.5", - "@angular/localize": "22.2.0-next.5", - "@angular/material": "22.2.0-next.4", - "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#c3d7ed184d71d390d3eacff85f4b2b3f16b82660", - "@angular/platform-browser": "22.2.0-next.5", - "@angular/platform-server": "22.2.0-next.5", - "@angular/router": "22.2.0-next.5", - "@angular/service-worker": "22.2.0-next.5", + "@angular/animations": "22.2.0-rc.0", + "@angular/cdk": "22.2.0-rc.0", + "@angular/common": "22.2.0-rc.0", + "@angular/compiler": "22.2.0-rc.0", + "@angular/core": "22.2.0-rc.0", + "@angular/forms": "22.2.0-rc.0", + "@angular/localize": "22.2.0-rc.0", + "@angular/material": "22.2.0-rc.0", + "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#f5de34cabca7e005dc8102d3abb026d01a7a881a", + "@angular/platform-browser": "22.2.0-rc.0", + "@angular/platform-server": "22.2.0-rc.0", + "@angular/router": "22.2.0-rc.0", + "@angular/service-worker": "22.2.0-rc.0", "@babel/core": "8.0.5", "@bazel/bazelisk": "1.28.1", "@bazel/buildifier": "8.2.1", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 25d640c7619f..9e0b0b7c4ff1 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -57,7 +57,7 @@ "istanbul-lib-instrument": "6.0.3", "jsdom": "30.0.1", "less": "4.9.1", - "ng-packagr": "22.2.0-next.5", + "ng-packagr": "22.2.0-rc.0", "postcss": "8.5.28", "rollup": "4.63.3", "rxjs": "7.8.2", diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index edd0ae7d22f0..299d7cf88ef4 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -7,8 +7,6 @@ */ import { BuilderContext } from '@angular-devkit/architect'; -import { existsSync } from 'node:fs'; -import path from 'node:path'; import { BuildOutputAsset, ExecutionResult, @@ -21,10 +19,8 @@ import { } from '../../tools/esbuild/stylesheets/sass-language'; import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils'; import { ChangedFiles } from '../../tools/esbuild/watcher'; -import { shouldWatchRoot } from '../../utils/environment-options'; import { initializeHash } from '../../utils/hash'; import { NormalizedCachedOptions } from '../../utils/normalize-cache'; -import { toPosixPath } from '../../utils/path'; import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; import { ComponentUpdateResult, @@ -35,20 +31,6 @@ import { ResultMessage, } from './results'; -// Watch workspace for package manager changes -const packageWatchFiles = [ - // manifest can affect module resolution - 'package.json', - // npm lock file - 'package-lock.json', - // pnpm lock file - 'pnpm-lock.yaml', - // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/) - 'yarn.lock', - '.pnp.cjs', - '.pnp.data.json', -]; - // eslint-disable-next-line max-lines-per-function export async function* runEsBuildBuildAction( action: (rebuildState?: RebuildState) => Promise, @@ -115,55 +97,18 @@ export async function* runEsBuildBuildAction( logger.info('Watch mode enabled. Watching for file changes...'); } - const normalizedOutputBase = toPosixPath(outputOptions.base); - const normalizedCacheBase = toPosixPath(cacheOptions.basePath); - const ignored: string[] = [ - // Ignore the output and cache paths to avoid infinite rebuild cycles - normalizedOutputBase, - `${normalizedOutputBase}/**`, - normalizedCacheBase, - `${normalizedCacheBase}/**`, - `${toPosixPath(workspaceRoot)}/**/.*/**`, - ]; - - if (cacheOptions.localBasePath && cacheOptions.localBasePath !== cacheOptions.basePath) { - const normalizedLocalCacheBase = toPosixPath(cacheOptions.localBasePath); - ignored.push(normalizedLocalCacheBase, `${normalizedLocalCacheBase}/**`); - } - // Setup a watcher - const { createWatcher } = await import('../../tools/esbuild/watcher'); - watcher = await createWatcher({ - polling: typeof poll === 'number', - interval: poll, - followSymlinks: preserveSymlinks, - ignored, - cwd: workspaceRoot, + const { setupWatcher } = await import('../../tools/esbuild/watcher'); + watcher = await setupWatcher({ + workspaceRoot, + projectRoot, + outputPath: outputOptions.base, + cacheOptions, + poll, + preserveSymlinks, + signal: options.signal, + watchFiles: result.watchFiles, }); - - // Setup abort support - options.signal?.addEventListener('abort', () => void watcher?.close()); - - // Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set - if (shouldWatchRoot) { - if (!preserveSymlinks) { - // Ignore all node modules directories to avoid excessive file watchers. - // Package changes are handled below by watching manifest and lock files. - // NOTE: this is not enable when preserveSymlinks is true as this would break `npm link` usages. - ignored.push('**/node_modules/**'); - - watcher.add( - packageWatchFiles - .map((file) => path.join(workspaceRoot, file)) - .filter((file) => existsSync(file)), - ); - } - - watcher.add(projectRoot); - } - - // Watch locations provided by the initial build result - watcher.add(result.watchFiles); } // Output the first build results after setting up the watcher to ensure that any code executed diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index d4656c046997..0d6ae77cdea7 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -17,7 +17,7 @@ import { } from '../../tools/esbuild/bundler-execution-result'; import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-files'; import { I18nInliner } from '../../tools/esbuild/i18n-inliner'; -import { maxWorkers } from '../../utils/environment-options'; +import { maxInlinerWorkers } from '../../utils/environment-options'; import { loadTranslations } from '../../utils/i18n-options'; import { createTranslationLoader } from '../../utils/load-translations'; import { createProjectResolver } from '../../utils/resolve-project'; @@ -51,7 +51,9 @@ export async function inlineI18n( const inliner = new I18nInliner( { missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning', - maxConcurrency: workerPool ? undefined : maxWorkers, + maxConcurrency: workerPool + ? Math.min(workerPool.maxThreads, maxInlinerWorkers) + : maxInlinerWorkers, persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined, localizeVersion: i18nOptions.localizeVersion, }, diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index dc53d55b61f0..785d07371d93 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -9,7 +9,6 @@ import type { BuilderContext } from '@angular-devkit/architect'; import type { Plugin } from 'esbuild'; import { access, constants, readFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; import path from 'node:path'; import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils'; import { supportColor } from '../../utils/color'; @@ -19,9 +18,8 @@ import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator' import { normalizeCacheOptions } from '../../utils/normalize-cache'; import { canonicalizePath } from '../../utils/path'; import { - SearchDirectory, - findTailwindConfiguration, generateSearchDirectories, + getTailwindConfig, loadPostcssConfiguration, } from '../../utils/postcss-configuration'; import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project-metadata'; @@ -280,7 +278,7 @@ export async function normalizeOptions( // Skip tailwind configuration if postcss is customized const tailwindConfiguration = postcssConfiguration ? undefined - : await getTailwindConfig(searchDirectories, workspaceRoot, context); + : await getTailwindConfig(searchDirectories, workspaceRoot, context.logger); let serverEntryPoint: string | undefined; if (typeof options.server === 'string') { @@ -538,36 +536,6 @@ export async function normalizeOptions( }; } -async function getTailwindConfig( - searchDirectories: SearchDirectory[], - workspaceRoot: string, - context: BuilderContext, -): Promise<{ file: string; package: string } | undefined> { - const tailwindConfigurationPath = findTailwindConfiguration(searchDirectories); - - if (!tailwindConfigurationPath) { - return undefined; - } - - // Create a node resolver from the configuration file - const resolver = createRequire(tailwindConfigurationPath); - try { - return { - file: tailwindConfigurationPath, - package: resolver.resolve('tailwindcss'), - }; - } catch { - const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath); - context.logger.warn( - `Tailwind CSS configuration file found (${relativeTailwindConfigPath})` + - ` but the 'tailwindcss' package is not installed.` + - ` To enable Tailwind CSS, please install the 'tailwindcss' package.`, - ); - } - - return undefined; -} - /** * Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser` * option which defines a single entry point. However, we also want to support multiple entry points as an internal option. diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts index a17c0a198e14..9608e0dc976e 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts @@ -38,8 +38,8 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expectNoLog(logs, 'Unexpected character "EOF"'); }, ], - { outputLogsOnFailure: false, timeout: 60_000 }, + { outputLogsOnFailure: false }, ); - }, 90_000); + }); }); }); diff --git a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts index 97f7d5cbe1c6..9761ff9aff64 100644 --- a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts @@ -31,6 +31,7 @@ import type { Loader, OnLoadResult, PartialMessage } from 'esbuild'; import { readFile, stat } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { mapConcurrent, runConcurrent } from '../../utils/concurrency'; import { calculateHash, createContentHash } from '../../utils/hash'; import type { Cache as PersistentCacheStore } from './cache'; import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache'; @@ -114,29 +115,6 @@ export function extractDiskFilePath(path: string): string | undefined { /** Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion. */ const MAX_CONCURRENT_READS = 16; -/** - * Maps an array asynchronously with a sliding worker pool to maintain full concurrency saturation. - */ -async function mapConcurrent( - items: T[], - limit: number, - fn: (item: T) => Promise, -): Promise { - const results: R[] = new Array(items.length); - let index = 0; - - const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { - while (index < items.length) { - const i = index++; - results[i] = await fn(items[i]); - } - }); - - await Promise.all(workers); - - return results; -} - /** * Validates that all imported watch files exist on disk and their contents match. * Performs a fast-path metadata check (mtime + size) first, falling back to content hashing. @@ -214,7 +192,7 @@ async function computeMetadataForWatchFiles( ): Promise> { const watchFilesMetadata: Record = {}; - await mapConcurrent(watchFiles, MAX_CONCURRENT_READS, async (filePath) => { + await runConcurrent(watchFiles, MAX_CONCURRENT_READS, async (filePath) => { try { const knownContent = knownContents?.get(filePath); const [content, stats] = await Promise.all([ diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts b/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts index 7fa20dde64ae..bb5c945bab7d 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts @@ -22,6 +22,7 @@ export interface BundleStylesheetOptions { workspaceRoot: string; optimization: boolean; inlineFonts: boolean; + dataurl?: boolean; preserveSymlinks?: boolean; sourcemap: boolean | 'external' | 'inline' | 'linked'; sourcesContent?: boolean; @@ -62,7 +63,7 @@ export function createStylesheetBundleOptions( pluginFactory.create(SassStylesheetLanguage), pluginFactory.create(LessStylesheetLanguage), pluginFactory.create(CssStylesheetLanguage), - createCssResourcePlugin(cache), + createCssResourcePlugin(cache, options.dataurl), ]; if (options.inlineFonts) { diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/css-resource-plugin.ts b/packages/angular/build/src/tools/esbuild/stylesheets/css-resource-plugin.ts index 7f83e7dc7a8f..ced6422c3fd3 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/css-resource-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/css-resource-plugin.ts @@ -25,9 +25,11 @@ const CSS_RESOURCE_RESOLUTION = Symbol('CSS_RESOURCE_RESOLUTION'); * and types to be supported without needing to manually specify all extensions * within the build configuration. * + * @param cache An optional load result cache. + * @param dataurl If true, resources will be loaded with the 'dataurl' loader to inline them as base64 data URIs. * @returns An esbuild {@link Plugin} instance. */ -export function createCssResourcePlugin(cache?: LoadResultCache): Plugin { +export function createCssResourcePlugin(cache?: LoadResultCache, dataurl?: boolean): Plugin { return { name: 'angular-css-resource', setup(build: PluginBuild): void { @@ -119,7 +121,7 @@ export function createCssResourcePlugin(cache?: LoadResultCache): Plugin { return { contents: await readFile(resourcePath), - loader: 'file', + loader: dataurl ? 'dataurl' : 'file', watchFiles: [resourcePath], }; }), diff --git a/packages/angular/build/src/tools/esbuild/watcher.ts b/packages/angular/build/src/tools/esbuild/watcher.ts index fe41275e479e..e30909fa6813 100644 --- a/packages/angular/build/src/tools/esbuild/watcher.ts +++ b/packages/angular/build/src/tools/esbuild/watcher.ts @@ -8,9 +8,11 @@ import type * as ParcelWatcher from '@parcel/watcher'; import type * as Chokidar from 'chokidar'; +import { once } from 'node:events'; import * as fs from 'node:fs'; import * as path from 'node:path'; import picomatch from 'picomatch'; +import { shouldWatchRoot } from '../../utils/environment-options'; import { toPosixPath } from '../../utils/path'; export class ChangedFiles { @@ -47,6 +49,108 @@ export interface WatcherOptions { cwd?: string; } +// Watch workspace for package manager changes +const packageWatchFiles = [ + // manifest can affect module resolution + 'package.json', + // npm lock file + 'package-lock.json', + // pnpm lock file + 'pnpm-lock.yaml', + // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/) + 'yarn.lock', + '.pnp.cjs', + '.pnp.data.json', +]; + +export interface SetupWatcherOptions { + workspaceRoot: string; + projectRoot: string; + outputPath: string; + cacheOptions: { basePath: string; localBasePath?: string }; + poll?: number; + preserveSymlinks?: boolean; + signal?: AbortSignal; + watchFiles?: Iterable; +} + +/** + * Sets up and initializes a file watcher with proper ignore patterns for build outputs and caches. + */ +export async function setupWatcher(options: SetupWatcherOptions): Promise { + const { + workspaceRoot, + projectRoot, + outputPath, + cacheOptions, + poll, + preserveSymlinks, + signal, + watchFiles, + } = options; + + const normalizedOutputBase = toPosixPath(outputPath); + const normalizedCacheBase = toPosixPath(cacheOptions.basePath); + const ignored: string[] = [ + // Ignore the output and cache paths to avoid infinite rebuild cycles + normalizedOutputBase, + `${normalizedOutputBase}/**`, + normalizedCacheBase, + `${normalizedCacheBase}/**`, + `${toPosixPath(workspaceRoot)}/**/.*/**`, + ]; + + if (cacheOptions.localBasePath && cacheOptions.localBasePath !== cacheOptions.basePath) { + const normalizedLocalCacheBase = toPosixPath(cacheOptions.localBasePath); + ignored.push(normalizedLocalCacheBase, `${normalizedLocalCacheBase}/**`); + } + + if (shouldWatchRoot && !preserveSymlinks) { + // Ignore all node modules directories to avoid excessive file watchers. + // Package changes are handled below by watching manifest and lock files. + // NOTE: this is not enabled when preserveSymlinks is true as this would break `npm link` usages. + ignored.push('**/node_modules/**'); + } + + const watcher = await createWatcher({ + polling: typeof poll === 'number', + interval: poll, + followSymlinks: preserveSymlinks, + ignored, + cwd: workspaceRoot, + }); + + // Setup abort support + if (signal) { + const onAbort = () => void watcher.close(); + signal.addEventListener('abort', onAbort, { once: true }); + const originalClose = watcher.close.bind(watcher); + watcher.close = async () => { + signal.removeEventListener('abort', onAbort); + await originalClose(); + }; + } + + // Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set + if (shouldWatchRoot) { + if (!preserveSymlinks) { + watcher.add( + packageWatchFiles + .map((file) => path.join(workspaceRoot, file)) + .filter((file) => fs.existsSync(file)), + ); + } + + watcher.add(projectRoot); + } + + if (watchFiles) { + watcher.add(Array.isArray(watchFiles) ? watchFiles : Array.from(watchFiles)); + } + + return watcher; +} + /** * Probes the filesystem at the specified target directory to determine whether it is case-sensitive. */ @@ -560,8 +664,15 @@ async function createChokidarWatcher( usePolling: !!options?.polling, interval: options?.interval, }); + const initTime = Date.now(); + // Wait for the watcher to complete its initial filesystem scan before returning. + // With `ignoreInitial: true`, any file visited during the initial scan is treated as the initial baseline + // and will not emit 'add' or 'change' events. Awaiting 'ready' ensures that rapid file modifications + // made right after watcher setup (e.g. in rebuild tests) are not swallowed as initial files. + await once(watcher, 'ready'); + const handleEvent = (type: 'added' | 'modified' | 'removed', rawPath: string) => { const posixPath = toPosixPathNormalized(rawPath); const lookupKey = toLookupKey(posixPath, isCaseSensitive); diff --git a/packages/angular/build/src/tools/esbuild/watcher_spec.ts b/packages/angular/build/src/tools/esbuild/watcher_spec.ts index 5c7853012762..e051c44bd3f2 100644 --- a/packages/angular/build/src/tools/esbuild/watcher_spec.ts +++ b/packages/angular/build/src/tools/esbuild/watcher_spec.ts @@ -16,6 +16,7 @@ import { createWatcher, getDirectoryPath, isPathInside, + setupWatcher, toPosixPathNormalized, } from './watcher'; @@ -117,6 +118,56 @@ describe('Watcher', () => { }); }); + describe('setupWatcher', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'setup-watcher-spec-'))); + }); + + afterEach(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it('should setup watcher with watchFiles and close on abort signal', async () => { + const abortController = new AbortController(); + const testFile = path.join(tempDir, 'main.ts'); + const watcher = await setupWatcher({ + workspaceRoot: tempDir, + projectRoot: tempDir, + outputPath: path.join(tempDir, 'dist'), + cacheOptions: { basePath: path.join(tempDir, '.cache') }, + watchFiles: [testFile], + signal: abortController.signal, + }); + + expect(watcher).toBeDefined(); + + const closeSpy = spyOn(watcher, 'close').and.callThrough(); + abortController.abort(); + + expect(closeSpy).toHaveBeenCalled(); + await watcher.close(); + }); + + it('should remove abort listener when watcher is closed', async () => { + const abortController = new AbortController(); + const removeSpy = spyOn(abortController.signal, 'removeEventListener').and.callThrough(); + const watcher = await setupWatcher({ + workspaceRoot: tempDir, + projectRoot: tempDir, + outputPath: path.join(tempDir, 'dist'), + cacheOptions: { basePath: path.join(tempDir, '.cache') }, + signal: abortController.signal, + }); + + await watcher.close(); + expect(removeSpy).toHaveBeenCalledWith('abort', jasmine.any(Function)); + }); + }); + describe('createWatcher', () => { let tempDir: string; diff --git a/packages/angular/build/src/utils/concurrency.ts b/packages/angular/build/src/utils/concurrency.ts new file mode 100644 index 000000000000..a38a9a57fa36 --- /dev/null +++ b/packages/angular/build/src/utils/concurrency.ts @@ -0,0 +1,93 @@ +/** + * @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 + */ + +/** + * Executes an asynchronous function for each item in an array concurrently up to a specified limit. + * + * If any task fails, processing of subsequent items stops and the first encountered error is re-thrown + * after all currently in-flight tasks have settled. + * + * @param items Array of items to process. + * @param limit Maximum number of concurrent tasks in flight. + * @param fn Async task function. + */ +export async function runConcurrent( + items: readonly T[], + limit: number, + fn: (item: T, index: number) => Promise, +): Promise { + if (items.length === 0) { + return; + } + + let index = 0; + let firstError: unknown; + + const concurrency = Math.min(Math.max(1, Math.floor(limit) || 1), items.length); + const workers = Array.from({ length: concurrency }, async () => { + while (!firstError && index < items.length) { + const i = index++; + try { + await fn(items[i], i); + } catch (error) { + firstError ??= error; + } + } + }); + + await Promise.allSettled(workers); + + if (firstError) { + throw firstError; + } +} + +/** + * Maps an array asynchronously with a sliding worker pool up to a specified concurrency limit. + * + * If any task fails, processing of subsequent items stops and the first encountered error is re-thrown + * after all currently in-flight tasks have settled. + * + * @param items Array of items to map. + * @param limit Maximum number of concurrent tasks in flight. + * @param fn Async mapper function. + * @returns Array of mapped results in the original item order. + */ +export async function mapConcurrent( + items: readonly T[], + limit: number, + fn: (item: T, index: number) => Promise, +): Promise { + if (items.length === 0) { + return []; + } + + const results: R[] = new Array(items.length); + let index = 0; + let firstError: unknown; + + const concurrency = Math.min(Math.max(1, Math.floor(limit) || 1), items.length); + const workers = Array.from({ length: concurrency }, async () => { + while (!firstError && index < items.length) { + const i = index++; + try { + results[i] = await fn(items[i], i); + } catch (error) { + firstError ??= error; + } + } + }); + + await Promise.allSettled(workers); + + if (firstError) { + throw firstError; + } + + return results; +} diff --git a/packages/angular/build/src/utils/concurrency_spec.ts b/packages/angular/build/src/utils/concurrency_spec.ts new file mode 100644 index 000000000000..ab63054046c5 --- /dev/null +++ b/packages/angular/build/src/utils/concurrency_spec.ts @@ -0,0 +1,212 @@ +/** + * @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 { mapConcurrent, runConcurrent } from './concurrency'; + +describe('concurrency utilities', () => { + describe('runConcurrent', () => { + it('should process all items in an array', async () => { + const items = [1, 2, 3, 4, 5]; + const processed: number[] = []; + + await runConcurrent(items, 2, async (item) => { + processed.push(item); + }); + + expect(processed.sort((a, b) => a - b)).toEqual(items); + }); + + it('should respect the concurrency limit', async () => { + const items = [10, 20, 30, 40, 50, 60]; + const limit = 2; + let active = 0; + let maxActive = 0; + + await runConcurrent(items, limit, async () => { + active++; + maxActive = Math.max(maxActive, active); + await new Promise((resolve) => setTimeout(resolve, 10)); + active--; + }); + + expect(maxActive).toBeLessThanOrEqual(limit); + }); + + it('should handle non-integer and NaN limits', async () => { + const items = [1, 2, 3]; + const processed: number[] = []; + + await runConcurrent(items, 2.7, async (item) => { + processed.push(item); + }); + expect(processed).toEqual(items); + + const processedNaN: number[] = []; + await runConcurrent(items, NaN, async (item) => { + processedNaN.push(item); + }); + expect(processedNaN).toEqual(items); + }); + + it('should handle an empty array', async () => { + let called = false; + await runConcurrent([], 3, async () => { + called = true; + }); + + expect(called).toBe(false); + }); + + it('should pass item and index to callback', async () => { + const items = ['a', 'b', 'c']; + const passed: { item: string; index: number }[] = []; + + await runConcurrent(items, 2, async (item, index) => { + passed.push({ item, index }); + }); + + expect(passed.sort((a, b) => a.index - b.index)).toEqual([ + { item: 'a', index: 0 }, + { item: 'b', index: 1 }, + { item: 'c', index: 2 }, + ]); + }); + + it('should stop processing new items and rethrow the first error', async () => { + const items = [1, 2, 3, 4, 5, 6]; + const executed: number[] = []; + + await expectAsync( + runConcurrent(items, 1, async (item) => { + executed.push(item); + if (item === 2) { + throw new Error('Task failed'); + } + }), + ).toBeRejectedWithError('Task failed'); + + // Subsequent items should not have been executed + expect(executed).toEqual([1, 2]); + }); + + it('should wait for in-flight tasks to settle when an error occurs', async () => { + const items = [1, 2, 3, 4]; + let task2Finished = false; + + await expectAsync( + runConcurrent(items, 2, async (item) => { + if (item === 1) { + throw new Error('Task 1 failed'); + } + if (item === 2) { + await new Promise((resolve) => setTimeout(resolve, 20)); + task2Finished = true; + } + }), + ).toBeRejectedWithError('Task 1 failed'); + + expect(task2Finished).toBe(true); + }); + }); + + describe('mapConcurrent', () => { + it('should map items and return results in original order', async () => { + const items = [1, 2, 3, 4, 5]; + + const results = await mapConcurrent(items, 2, async (item) => { + // Add varying delay so tasks finish out of order + await new Promise((resolve) => setTimeout(resolve, (5 - item) * 5)); + + return item * 2; + }); + + expect(results).toEqual([2, 4, 6, 8, 10]); + }); + + it('should respect the concurrency limit', async () => { + const items = [1, 2, 3, 4, 5]; + const limit = 2; + let active = 0; + let maxActive = 0; + + await mapConcurrent(items, limit, async (item) => { + active++; + maxActive = Math.max(maxActive, active); + await new Promise((resolve) => setTimeout(resolve, 10)); + active--; + + return item; + }); + + expect(maxActive).toBeLessThanOrEqual(limit); + }); + + it('should handle non-integer and NaN limits', async () => { + const items = [1, 2, 3]; + + const results = await mapConcurrent(items, 2.7, async (item) => item * 2); + expect(results).toEqual([2, 4, 6]); + + const resultsNaN = await mapConcurrent(items, NaN, async (item) => item * 2); + expect(resultsNaN).toEqual([2, 4, 6]); + }); + + it('should handle an empty array', async () => { + const results = await mapConcurrent([], 3, async (item) => item); + + expect(results).toEqual([]); + }); + + it('should pass item and index to mapper function', async () => { + const items = ['x', 'y', 'z']; + + const results = await mapConcurrent(items, 2, async (item, index) => `${item}:${index}`); + + expect(results).toEqual(['x:0', 'y:1', 'z:2']); + }); + + it('should stop processing new items and rethrow the first error', async () => { + const items = [1, 2, 3, 4, 5, 6]; + const executed: number[] = []; + + await expectAsync( + mapConcurrent(items, 1, async (item) => { + executed.push(item); + if (item === 2) { + throw new Error('Map failed'); + } + + return item; + }), + ).toBeRejectedWithError('Map failed'); + + expect(executed).toEqual([1, 2]); + }); + + it('should wait for in-flight tasks to settle when an error occurs', async () => { + const items = [1, 2, 3, 4]; + let task2Finished = false; + + await expectAsync( + mapConcurrent(items, 2, async (item) => { + if (item === 1) { + throw new Error('Map 1 failed'); + } + if (item === 2) { + await new Promise((resolve) => setTimeout(resolve, 20)); + task2Finished = true; + } + + return item; + }), + ).toBeRejectedWithError('Map 1 failed'); + + expect(task2Finished).toBe(true); + }); + }); +}); diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index 29faf3e7bbae..d87c104aba65 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -141,6 +141,15 @@ export const maxWorkers = customMaxWorkers ?? Math.max(availableParallelism() - export const maxTransformWorkers = customMaxWorkers ?? Math.max(1, Math.min(6, Math.floor(availableParallelism() / 4))); +/** + * The maximum number of workers to use for i18n translation inlining. + * Translation inlining and sourcemap remapping are CPU- and memory-intensive operations. + * To prevent thread oversubscription, memory allocator lock contention, and high-core + * performance degradation, concurrency is capped at 8 unless overridden by + * `NG_BUILD_MAX_WORKERS`. + */ +export const maxInlinerWorkers = customMaxWorkers ?? Math.min(8, availableParallelism()); + /** * When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled. */ diff --git a/packages/angular/build/src/utils/environment-options_spec.ts b/packages/angular/build/src/utils/environment-options_spec.ts index 4146bec8bec1..4ad098a24763 100644 --- a/packages/angular/build/src/utils/environment-options_spec.ts +++ b/packages/angular/build/src/utils/environment-options_spec.ts @@ -181,3 +181,67 @@ describe('environment options - maxTransformWorkers', () => { expect(maxTransformWorkers).toBe(expected); }); }); + +describe('environment options - maxInlinerWorkers', () => { + const originalEnvValue = process.env['NG_BUILD_MAX_WORKERS']; + + function loadEnvironmentOptions(): typeof import('./environment-options') { + delete require.cache[require.resolve('./environment-options')]; + + return require('./environment-options'); + } + + afterEach(() => { + if (originalEnvValue !== undefined) { + process.env['NG_BUILD_MAX_WORKERS'] = originalEnvValue; + } else { + delete process.env['NG_BUILD_MAX_WORKERS']; + } + delete require.cache[require.resolve('./environment-options')]; + }); + + it('defaults maxInlinerWorkers to min(8, availableParallelism()) when NG_BUILD_MAX_WORKERS is unset', () => { + delete process.env['NG_BUILD_MAX_WORKERS']; + const { maxInlinerWorkers } = loadEnvironmentOptions(); + + expect(maxInlinerWorkers).toBe(Math.min(8, availableParallelism())); + }); + + it('uses configured positive integer when NG_BUILD_MAX_WORKERS is set', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '4'; + const { maxInlinerWorkers } = loadEnvironmentOptions(); + + expect(maxInlinerWorkers).toBe(4); + }); + + it('allows maxInlinerWorkers greater than 8 when explicitly configured', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '16'; + const { maxInlinerWorkers } = loadEnvironmentOptions(); + + expect(maxInlinerWorkers).toBe(16); + }); + + it('supports maxInlinerWorkers set to 1', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '1'; + const { maxInlinerWorkers } = loadEnvironmentOptions(); + + expect(maxInlinerWorkers).toBe(1); + }); + + it('falls back to min(8, availableParallelism()) when NG_BUILD_MAX_WORKERS is 0 or negative', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '0'; + const { maxInlinerWorkers: zeroWorkers } = loadEnvironmentOptions(); + expect(zeroWorkers).toBe(Math.min(8, availableParallelism())); + + process.env['NG_BUILD_MAX_WORKERS'] = '-4'; + const { maxInlinerWorkers: negativeWorkers } = loadEnvironmentOptions(); + expect(negativeWorkers).toBe(Math.min(8, availableParallelism())); + }); + + it('falls back to min(8, availableParallelism()) when NG_BUILD_MAX_WORKERS is invalid', () => { + process.env['NG_BUILD_MAX_WORKERS'] = 'invalid'; + const { maxInlinerWorkers } = loadEnvironmentOptions(); + + expect(maxInlinerWorkers).toBe(Math.min(8, availableParallelism())); + }); +}); 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 df33f0766607..5c1899015a7e 100644 --- a/packages/angular/build/src/utils/index-file/auto-csp.ts +++ b/packages/angular/build/src/utils/index-file/auto-csp.ts @@ -36,15 +36,40 @@ function getScriptAttributeValue(tag: StartTag, attrName: string): string | unde return tag.attrs.find((attr) => attr.name === attrName)?.value; } +/** + * All MIME types associated with JavaScript according to the HTML specification: + * https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type + */ +const JAVASCRIPT_MIME_TYPES = new Set([ + 'application/ecmascript', + 'application/javascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + 'text/javascript1.0', + 'text/javascript1.1', + 'text/javascript1.2', + 'text/javascript1.3', + 'text/javascript1.4', + 'text/javascript1.5', + 'text/jscript', + 'text/livescript', + 'text/x-ecmascript', + 'text/x-javascript', +]); + /** * Checks whether a particular string is a MIME type associated with JavaScript, according to - * https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types#textjavascript + * https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type * * @param mimeType a string that may be a MIME type * @returns whether the string is a MIME type that is associated with JavaScript */ -function isJavascriptMimeType(mimeType: string): boolean { - return mimeType.split(';')[0] === 'text/javascript'; +export function isJavascriptMimeType(mimeType: string): boolean { + const [essence] = mimeType.split(';', 1); + + return JAVASCRIPT_MIME_TYPES.has(essence.trim().toLowerCase()); } /** @@ -54,7 +79,11 @@ function isJavascriptMimeType(mimeType: string): boolean { * @returns whether to add the script tag to the dynamically loaded script tag */ function shouldDynamicallyLoadScriptTagBasedOnType(scriptType: string | undefined): boolean { - return !scriptType || scriptType === 'module' || isJavascriptMimeType(scriptType); + if (!scriptType) { + return true; + } + + return scriptType.trim().toLowerCase() === 'module' || isJavascriptMimeType(scriptType); } /** 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 29e9bff68074..4fe2888efa42 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 @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { autoCsp, hashTextContent } from './auto-csp'; +import { autoCsp, hashTextContent, isJavascriptMimeType } from './auto-csp'; // Utility function to grab the meta tag CSPs from the HTML response. const getCsps = (html: string) => { @@ -281,4 +281,121 @@ describe('auto-csp', () => { `const scripts = [['./main.js', '', false, false, null, "anonymous"]];`, ); }); + + it('should rewrite scripts with application/javascript type', async () => { + const result = await autoCsp(` + + + + + + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', 'application/javascript', false, false, null, null]];`, + ); + }); + + it('should rewrite scripts with case-insensitive type and parameters with whitespace', async () => { + const result = await autoCsp(` + + + + + + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', 'Text/JavaScript ; charset=utf-8', false, false, null, null]];`, + ); + }); + + it('should rewrite scripts with case-insensitive module type', async () => { + const result = await autoCsp(` + + + + + + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', 'Module', false, false, null, null]];`, + ); + }); + + it('should not rewrite non-JavaScript script tags', async () => { + const result = await autoCsp(` + + + + + + + `); + + // No dynamic loader script is emitted because application/json is not JavaScript. + expect(result).toContain(''); + expect(result).not.toContain('const scripts ='); + }); + + describe('isJavascriptMimeType', () => { + it('should identify standard JavaScript MIME types', () => { + expect(isJavascriptMimeType('text/javascript')).toBeTrue(); + expect(isJavascriptMimeType('application/javascript')).toBeTrue(); + expect(isJavascriptMimeType('application/x-javascript')).toBeTrue(); + expect(isJavascriptMimeType('text/ecmascript')).toBeTrue(); + expect(isJavascriptMimeType('application/ecmascript')).toBeTrue(); + expect(isJavascriptMimeType('text/jscript')).toBeTrue(); + expect(isJavascriptMimeType('text/livescript')).toBeTrue(); + expect(isJavascriptMimeType('text/x-ecmascript')).toBeTrue(); + expect(isJavascriptMimeType('text/x-javascript')).toBeTrue(); + expect(isJavascriptMimeType('text/javascript1.5')).toBeTrue(); + }); + + it('should ignore parameters when matching MIME type', () => { + expect(isJavascriptMimeType('text/javascript; charset=utf-8')).toBeTrue(); + expect(isJavascriptMimeType('application/javascript;version=1.8')).toBeTrue(); + }); + + it('should handle leading, trailing, and parameter whitespace', () => { + expect(isJavascriptMimeType(' text/javascript ')).toBeTrue(); + expect(isJavascriptMimeType('text/javascript ; charset=utf-8')).toBeTrue(); + expect(isJavascriptMimeType(' application/javascript ; version=1.0 ')).toBeTrue(); + }); + + it('should be case-insensitive', () => { + expect(isJavascriptMimeType('Text/JavaScript')).toBeTrue(); + expect(isJavascriptMimeType('APPLICATION/JAVASCRIPT')).toBeTrue(); + expect(isJavascriptMimeType('text/JAVASCRIPT; charset=UTF-8')).toBeTrue(); + }); + + it('should reject non-JavaScript MIME types', () => { + expect(isJavascriptMimeType('application/json')).toBeFalse(); + expect(isJavascriptMimeType('text/html')).toBeFalse(); + expect(isJavascriptMimeType('text/css')).toBeFalse(); + expect(isJavascriptMimeType('image/svg+xml')).toBeFalse(); + expect(isJavascriptMimeType('importmap')).toBeFalse(); + expect(isJavascriptMimeType('module')).toBeFalse(); + expect(isJavascriptMimeType('')).toBeFalse(); + }); + + it('should reject invalid MIME types with whitespace inside the essence', () => { + expect(isJavascriptMimeType('text / javascript')).toBeFalse(); + expect(isJavascriptMimeType('application / javascript')).toBeFalse(); + expect(isJavascriptMimeType('text/java script')).toBeFalse(); + }); + }); }); diff --git a/packages/angular/build/src/utils/postcss-configuration.ts b/packages/angular/build/src/utils/postcss-configuration.ts index 6f3f1f3671f9..abde0e632b30 100644 --- a/packages/angular/build/src/utils/postcss-configuration.ts +++ b/packages/angular/build/src/utils/postcss-configuration.ts @@ -7,7 +7,8 @@ */ import { readFile, readdir } from 'node:fs/promises'; -import { join } from 'node:path'; +import { createRequire } from 'node:module'; +import { join, relative } from 'node:path'; export interface PostcssConfiguration { plugins: [name: string, options?: object | string][]; @@ -62,6 +63,35 @@ export function findTailwindConfiguration( return findFile(searchDirectories, tailwindConfigFiles); } +export async function getTailwindConfig( + searchDirectories: SearchDirectory[], + workspaceRoot: string, + logger?: { warn(message: string): void }, +): Promise<{ file: string; package: string } | undefined> { + const tailwindConfigurationPath = findTailwindConfiguration(searchDirectories); + if (!tailwindConfigurationPath) { + return undefined; + } + + // Create a node resolver from the configuration file + const resolver = createRequire(tailwindConfigurationPath); + try { + return { + file: tailwindConfigurationPath, + package: resolver.resolve('tailwindcss'), + }; + } catch { + const relativeTailwindConfigPath = relative(workspaceRoot, tailwindConfigurationPath); + logger?.warn( + `Tailwind CSS configuration file found (${relativeTailwindConfigPath})` + + ` but the 'tailwindcss' package is not installed.` + + ` To enable Tailwind CSS, please install the 'tailwindcss' package.`, + ); + } + + return undefined; +} + async function readPostcssConfiguration( configurationFile: string, ): Promise { diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 35e0d4f8201d..ae0eb82f0cb5 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -7,6 +7,8 @@ */ import { schema } from '@angular-devkit/core'; +import { readFileSync } from 'node:fs'; +import { join, posix, relative } from 'node:path'; import type { ArgumentsCamelCase, Argv, CommandModule as YargsCommandModule } from 'yargs'; import { Parser as yargsParser } from 'yargs/helpers'; import { getAnalyticsUserId } from '../analytics/analytics'; @@ -18,7 +20,6 @@ import { AngularWorkspace } from '../utilities/config'; import { memoize } from '../utilities/memoize'; import { CommandContext, CommandScope, Options, OtherOptions } from './definitions'; import { Option, addSchemaOptionsToCommand } from './utilities/json-schema'; -import '../utilities/markdown-loader'; export { CommandScope }; export type { CommandContext, Options, OtherOptions }; @@ -30,11 +31,8 @@ export interface CommandModuleImplementation extends Omit< /** Scope in which the command can be executed in. */ scope: CommandScope; - /** Long description for the command in JSON help text. */ - longDescription?: string; - - /** Relative path to the long description file for the command in JSON help text. */ - longDescriptionRelativePath?: string; + /** Path used to load the long description for the command in JSON help text. */ + longDescriptionPath?: string; /** Object declaring the options the command accepts, or a function accepting and returning a yargs instance. */ builder(argv: Argv): Promise> | Argv; @@ -52,8 +50,7 @@ export interface FullDescribe { export abstract class CommandModule implements CommandModuleImplementation { abstract readonly command: string; abstract readonly describe: string | false; - readonly longDescription?: string; - readonly longDescriptionRelativePath?: string; + abstract readonly longDescriptionPath?: string; protected readonly shouldReportAnalytics: boolean = true; readonly scope: CommandScope = CommandScope.Both; @@ -71,22 +68,23 @@ export abstract class CommandModule implements CommandModuleI * `false` will result in a hidden command. */ public get fullDescribe(): FullDescribe | false { - if (this.describe === false) { - return false; - } - - const description: FullDescribe = { - describe: this.describe, - }; - - if (this.longDescription) { - description.longDescription = this.longDescription.replace(/\r\n/g, '\n'); - description.longDescriptionRelativePath = - this.longDescriptionRelativePath ?? - `@angular/cli/src/commands/${this.commandName}/long-description.md`; - } - - return description; + return this.describe === false + ? false + : { + describe: this.describe, + ...(this.longDescriptionPath + ? { + longDescriptionRelativePath: relative( + join(__dirname, '../../../../'), + this.longDescriptionPath, + ).replace(/\\/g, posix.sep), + longDescription: readFileSync(this.longDescriptionPath, 'utf8').replace( + /\r\n/g, + '\n', + ), + } + : {}), + }; } protected get commandName(): string { diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index fb352f70edfd..bc95f33ff8e7 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -29,9 +29,6 @@ import { NgAddSaveDependency, PackageManifest, PackageMetadata } from '../../pac import { assertIsError } from '../../utilities/error'; import { isTTY } from '../../utilities/tty'; import { VERSION } from '../../utilities/version'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; class CommandError extends Error {} @@ -103,7 +100,7 @@ export default class AddCommandModule { command = 'add '; describe = 'Adds support for an external library to your project.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); protected override allowPrivateSchematics = true; private readonly schematicName = 'ng-add'; private rootRequire = createRequire(this.context.root + '/'); diff --git a/packages/angular/cli/src/commands/analytics/cli.ts b/packages/angular/cli/src/commands/analytics/cli.ts index 9639cbab50e6..da56a2a00460 100644 --- a/packages/angular/cli/src/commands/analytics/cli.ts +++ b/packages/angular/cli/src/commands/analytics/cli.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { Argv } from 'yargs'; import { CommandModule, @@ -17,9 +18,6 @@ import { demandCommandFailureMessage, } from '../../command-builder/utilities/command'; import { AnalyticsInfoCommandModule } from './info/cli'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; import { AnalyticsDisableModule, AnalyticsEnableModule, @@ -32,7 +30,7 @@ export default class AnalyticsCommandModule { command = 'analytics'; describe = 'Configures the gathering of Angular CLI usage metrics.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); builder(localYargs: Argv): Argv { const subcommands = [ diff --git a/packages/angular/cli/src/commands/analytics/info/cli.ts b/packages/angular/cli/src/commands/analytics/info/cli.ts index 93b34fd06716..e4434d35baee 100644 --- a/packages/angular/cli/src/commands/analytics/info/cli.ts +++ b/packages/angular/cli/src/commands/analytics/info/cli.ts @@ -20,6 +20,7 @@ export class AnalyticsInfoCommandModule { command = 'info'; describe = 'Prints analytics gathering and reporting configuration in the console.'; + longDescriptionPath?: string; builder(localYargs: Argv): Argv { return localYargs.strict(); diff --git a/packages/angular/cli/src/commands/analytics/settings/cli.ts b/packages/angular/cli/src/commands/analytics/settings/cli.ts index 24105de2b060..16f07b353d1a 100644 --- a/packages/angular/cli/src/commands/analytics/settings/cli.ts +++ b/packages/angular/cli/src/commands/analytics/settings/cli.ts @@ -26,6 +26,8 @@ abstract class AnalyticsSettingModule extends CommandModule implements CommandModuleImplementation { + longDescriptionPath?: string; + builder(localYargs: Argv): Argv { return localYargs .option('global', { diff --git a/packages/angular/cli/src/commands/build/cli.ts b/packages/angular/cli/src/commands/build/cli.ts index 031e31b5140f..365420ca3734 100644 --- a/packages/angular/cli/src/commands/build/cli.ts +++ b/packages/angular/cli/src/commands/build/cli.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; import { CommandModuleImplementation } from '../../command-builder/command-module'; import { RootCommands } from '../command-config'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; export default class BuildCommandModule extends ArchitectCommandModule @@ -22,5 +20,5 @@ export default class BuildCommandModule aliases = RootCommands['build'].aliases; describe = 'Compiles an Angular application or library into an output directory named dist/ at the given output path.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); } diff --git a/packages/angular/cli/src/commands/cache/clean/cli.ts b/packages/angular/cli/src/commands/cache/clean/cli.ts index 492699271b54..a115b686b7e0 100644 --- a/packages/angular/cli/src/commands/cache/clean/cli.ts +++ b/packages/angular/cli/src/commands/cache/clean/cli.ts @@ -18,6 +18,7 @@ import { getCacheConfig } from '../utilities'; export class CacheCleanModule extends CommandModule implements CommandModuleImplementation { command = 'clean'; describe = 'Deletes persistent disk cache from disk.'; + longDescriptionPath: string | undefined; override scope = CommandScope.In; builder(localYargs: Argv): Argv { diff --git a/packages/angular/cli/src/commands/cache/cli.ts b/packages/angular/cli/src/commands/cache/cli.ts index 25140041ae0e..dad144b034b3 100644 --- a/packages/angular/cli/src/commands/cache/cli.ts +++ b/packages/angular/cli/src/commands/cache/cli.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { Argv } from 'yargs'; import { CommandModule, @@ -19,9 +20,6 @@ import { } from '../../command-builder/utilities/command'; import { CacheCleanModule } from './clean/cli'; import { CacheInfoCommandModule } from './info/cli'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; import { CacheDisableModule, CacheEnableModule } from './settings/cli'; export default class CacheCommandModule @@ -30,7 +28,7 @@ export default class CacheCommandModule { command = 'cache'; describe = 'Configure persistent disk cache and retrieve cache statistics.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); override scope = CommandScope.In; builder(localYargs: Argv): Argv { diff --git a/packages/angular/cli/src/commands/cache/info/cli.ts b/packages/angular/cli/src/commands/cache/info/cli.ts index 6fd2df0414c7..f4278d52db74 100644 --- a/packages/angular/cli/src/commands/cache/info/cli.ts +++ b/packages/angular/cli/src/commands/cache/info/cli.ts @@ -21,6 +21,7 @@ import { getCacheConfig } from '../utilities'; export class CacheInfoCommandModule extends CommandModule implements CommandModuleImplementation { command = 'info'; describe = 'Prints persistent disk cache configuration and statistics in the console.'; + longDescriptionPath?: string | undefined; override scope = CommandScope.In; builder(localYargs: Argv): Argv { diff --git a/packages/angular/cli/src/commands/cache/settings/cli.ts b/packages/angular/cli/src/commands/cache/settings/cli.ts index 5de2d5496475..9a4f654f7ac7 100644 --- a/packages/angular/cli/src/commands/cache/settings/cli.ts +++ b/packages/angular/cli/src/commands/cache/settings/cli.ts @@ -18,6 +18,7 @@ export class CacheDisableModule extends CommandModule implements CommandModuleIm command = 'disable'; aliases = 'off'; describe = 'Disables persistent disk cache for all projects in the workspace.'; + longDescriptionPath: string | undefined; override scope = CommandScope.In; builder(localYargs: Argv): Argv { @@ -33,6 +34,7 @@ export class CacheEnableModule extends CommandModule implements CommandModuleImp command = 'enable'; aliases = 'on'; describe = 'Enables disk cache for all projects in the workspace.'; + longDescriptionPath: string | undefined; override scope = CommandScope.In; builder(localYargs: Argv): Argv { diff --git a/packages/angular/cli/src/commands/completion/cli.ts b/packages/angular/cli/src/commands/completion/cli.ts index ab81ce5636d8..3fc9dccdc703 100644 --- a/packages/angular/cli/src/commands/completion/cli.ts +++ b/packages/angular/cli/src/commands/completion/cli.ts @@ -6,15 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { Argv } from 'yargs'; import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; import { addCommandModuleToYargs } from '../../command-builder/utilities/command'; import { colors } from '../../utilities/color'; import { hasGlobalCliInstall, initializeAutocomplete } from '../../utilities/completion'; import { assertIsError } from '../../utilities/error'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; export default class CompletionCommandModule extends CommandModule @@ -22,7 +20,7 @@ export default class CompletionCommandModule { command = 'completion'; describe = 'Set up Angular CLI autocompletion for your terminal.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); builder(localYargs: Argv): Argv { addCommandModuleToYargs(CompletionScriptCommandModule, this.context); @@ -66,6 +64,7 @@ Appended \`source <(ng completion script)\` to \`${rcFile}\`. Restart your termi class CompletionScriptCommandModule extends CommandModule implements CommandModuleImplementation { command = 'script'; describe = 'Generate a bash and zsh real-time type-ahead autocompletion script.'; + longDescriptionPath = undefined; builder(localYargs: Argv): Argv { return localYargs; diff --git a/packages/angular/cli/src/commands/config/cli.ts b/packages/angular/cli/src/commands/config/cli.ts index f339fd9bb1ba..06b253b9a42d 100644 --- a/packages/angular/cli/src/commands/config/cli.ts +++ b/packages/angular/cli/src/commands/config/cli.ts @@ -8,6 +8,7 @@ import { JsonValue } from '@angular-devkit/core'; import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; import { Argv } from 'yargs'; import { CommandModule, @@ -17,9 +18,6 @@ import { } from '../../command-builder/command-module'; import { getWorkspaceRaw, validateWorkspace } from '../../utilities/config'; import { JSONFile, parseJson } from '../../utilities/json-file'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; interface ConfigCommandArgs { 'json-path'?: string; @@ -34,7 +32,7 @@ export default class ConfigCommandModule command = 'config [json-path] [value]'; describe = 'Retrieves or sets Angular configuration values in the angular.json file for the workspace.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); builder(localYargs: Argv): Argv { return localYargs diff --git a/packages/angular/cli/src/commands/deploy/cli.ts b/packages/angular/cli/src/commands/deploy/cli.ts index 28317cff1ee1..947dc90af2d4 100644 --- a/packages/angular/cli/src/commands/deploy/cli.ts +++ b/packages/angular/cli/src/commands/deploy/cli.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { MissingTargetChoice } from '../../command-builder/architect-base-command-module'; import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; import { CommandModuleImplementation } from '../../command-builder/command-module'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; export default class DeployCommandModule extends ArchitectCommandModule @@ -39,7 +37,7 @@ export default class DeployCommandModule multiTarget = false; command = 'deploy [project]'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); describe = 'Invokes the deploy builder for a specified project or for the default project in the workspace.'; } diff --git a/packages/angular/cli/src/commands/e2e/cli.ts b/packages/angular/cli/src/commands/e2e/cli.ts index 9d63a8d96fb4..85d9aab173a0 100644 --- a/packages/angular/cli/src/commands/e2e/cli.ts +++ b/packages/angular/cli/src/commands/e2e/cli.ts @@ -42,4 +42,5 @@ export default class E2eCommandModule command = 'e2e [project]'; aliases = RootCommands['e2e'].aliases; describe = 'Builds and serves an Angular application, then runs end-to-end tests.'; + longDescriptionPath?: string; } diff --git a/packages/angular/cli/src/commands/extract-i18n/cli.ts b/packages/angular/cli/src/commands/extract-i18n/cli.ts index 09b3df343403..4f3dea2d8e7e 100644 --- a/packages/angular/cli/src/commands/extract-i18n/cli.ts +++ b/packages/angular/cli/src/commands/extract-i18n/cli.ts @@ -19,6 +19,7 @@ export default class ExtractI18nCommandModule multiTarget = false; command = 'extract-i18n [project]'; describe = 'Extracts i18n messages from source code.'; + longDescriptionPath?: string | undefined; override async findDefaultBuilderName( project: workspaces.ProjectDefinition, diff --git a/packages/angular/cli/src/commands/generate/cli.ts b/packages/angular/cli/src/commands/generate/cli.ts index 8f605e03a18b..4be29c3eaea0 100644 --- a/packages/angular/cli/src/commands/generate/cli.ts +++ b/packages/angular/cli/src/commands/generate/cli.ts @@ -38,6 +38,7 @@ export default class GenerateCommandModule command = 'generate'; aliases = RootCommands['generate'].aliases; describe = 'Generates and/or modifies files based on a schematic.'; + longDescriptionPath?: string | undefined; override async builder(argv: Argv): Promise> { let localYargs = (await super.builder(argv)).command({ diff --git a/packages/angular/cli/src/commands/lint/cli.ts b/packages/angular/cli/src/commands/lint/cli.ts index 3fdd38fc169c..9510dd7afe53 100644 --- a/packages/angular/cli/src/commands/lint/cli.ts +++ b/packages/angular/cli/src/commands/lint/cli.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { MissingTargetChoice } from '../../command-builder/architect-base-command-module'; import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; import { CommandModuleImplementation } from '../../command-builder/command-module'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; export default class LintCommandModule extends ArchitectCommandModule @@ -26,6 +24,6 @@ export default class LintCommandModule multiTarget = true; command = 'lint [project]'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); describe = 'Runs linting tools on Angular application code in a given project folder.'; } diff --git a/packages/angular/cli/src/commands/make-this-awesome/cli.ts b/packages/angular/cli/src/commands/make-this-awesome/cli.ts index 8e36447f1f3a..6a17c5614b94 100644 --- a/packages/angular/cli/src/commands/make-this-awesome/cli.ts +++ b/packages/angular/cli/src/commands/make-this-awesome/cli.ts @@ -17,6 +17,7 @@ export default class AwesomeCommandModule command = 'make-this-awesome'; describe = false as const; deprecated = false; + longDescriptionPath?: string | undefined; builder(localYargs: Argv): Argv { return localYargs; diff --git a/packages/angular/cli/src/commands/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts index cbc94499a9ca..a379022e8f5e 100644 --- a/packages/angular/cli/src/commands/mcp/cli.ts +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -35,6 +35,7 @@ For more information and documentation, visit: https://angular.dev/ai/mcp export default class McpCommandModule extends CommandModule implements CommandModuleImplementation { command = 'mcp'; describe = false as const; + longDescriptionPath = undefined; builder(localYargs: Argv): Argv { return localYargs diff --git a/packages/angular/cli/src/commands/new/cli.ts b/packages/angular/cli/src/commands/new/cli.ts index c47f3c63153b..6e6545e66421 100644 --- a/packages/angular/cli/src/commands/new/cli.ts +++ b/packages/angular/cli/src/commands/new/cli.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { Argv } from 'yargs'; import { CommandModuleImplementation, @@ -20,9 +21,6 @@ import { } from '../../command-builder/schematics-command-module'; import { VERSION } from '../../utilities/version'; import { RootCommands } from '../command-config'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; interface NewCommandArgs extends SchematicsCommandArgs { collection?: string; @@ -39,7 +37,7 @@ export default class NewCommandModule command = 'new [name]'; aliases = RootCommands['new'].aliases; describe = 'Creates a new Angular workspace.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); override async builder(argv: Argv): Promise> { const localYargs = (await super.builder(argv)).option('collection', { diff --git a/packages/angular/cli/src/commands/run/cli.ts b/packages/angular/cli/src/commands/run/cli.ts index 774f811aa2a5..aa12cd0158f7 100644 --- a/packages/angular/cli/src/commands/run/cli.ts +++ b/packages/angular/cli/src/commands/run/cli.ts @@ -7,6 +7,7 @@ */ import { Target } from '@angular-devkit/architect'; +import { join } from 'node:path'; import { Argv } from 'yargs'; import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module'; import { @@ -16,9 +17,6 @@ import { Options, OtherOptions, } from '../../command-builder/command-module'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; export interface RunCommandArgs { target: string; @@ -33,7 +31,7 @@ export default class RunCommandModule command = 'run '; describe = 'Runs an Architect target with an optional custom builder configuration defined in your project.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); async builder(argv: Argv): Promise> { const { jsonHelp, getYargsCompletions, help } = this.context.args.options; diff --git a/packages/angular/cli/src/commands/serve/cli.ts b/packages/angular/cli/src/commands/serve/cli.ts index 6448c48b9484..3b38fa122acd 100644 --- a/packages/angular/cli/src/commands/serve/cli.ts +++ b/packages/angular/cli/src/commands/serve/cli.ts @@ -18,4 +18,5 @@ export default class ServeCommandModule command = 'serve [project]'; aliases = RootCommands['serve'].aliases; describe = 'Builds and serves your application, rebuilding on file changes.'; + longDescriptionPath?: string | undefined; } diff --git a/packages/angular/cli/src/commands/test/cli.ts b/packages/angular/cli/src/commands/test/cli.ts index f9dcaaf92c8c..600e9f41f517 100644 --- a/packages/angular/cli/src/commands/test/cli.ts +++ b/packages/angular/cli/src/commands/test/cli.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import { join } from 'node:path'; import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; import { CommandModuleImplementation } from '../../command-builder/command-module'; import { RootCommands } from '../command-config'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; export default class TestCommandModule extends ArchitectCommandModule @@ -21,5 +19,5 @@ export default class TestCommandModule command = 'test [project]'; aliases = RootCommands['test'].aliases; describe = 'Runs unit tests in a project.'; - override longDescription = longDescription; + longDescriptionPath = join(__dirname, 'long-description.md'); } diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts index 6418bfc13ce4..005df998b501 100644 --- a/packages/angular/cli/src/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -24,9 +24,6 @@ import type { InstalledPackage, PackageManager, PackageManifest } from '../../pa import { colors } from '../../utilities/color'; import { disableVersionCheck } from '../../utilities/environment-options'; import { assertIsError } from '../../utilities/error'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore strict-deps: Markdown files are asset dependencies bundled/loaded at runtime -import longDescription from './long-description.md'; import { UpdatePlan, applyUpdatePlan, @@ -66,7 +63,7 @@ export default class UpdateCommandModule extends CommandModule { return localYargs diff --git a/packages/angular/cli/src/commands/version/cli.ts b/packages/angular/cli/src/commands/version/cli.ts index 1a79b8b76eef..205f0bc7e55e 100644 --- a/packages/angular/cli/src/commands/version/cli.ts +++ b/packages/angular/cli/src/commands/version/cli.ts @@ -37,6 +37,7 @@ export default class VersionCommandModule command = 'version'; aliases = RootCommands['version'].aliases; describe = 'Outputs Angular CLI version.'; + longDescriptionPath?: string | undefined; /** * Builds the command-line options for the `ng version` command. diff --git a/packages/angular/cli/src/typings.d.ts b/packages/angular/cli/src/typings.d.ts deleted file mode 100644 index 12e7de03c204..000000000000 --- a/packages/angular/cli/src/typings.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -declare module '*/long-description.md' { - const content: string; - export default content; -} diff --git a/packages/angular/cli/src/utilities/markdown-loader.ts b/packages/angular/cli/src/utilities/markdown-loader.ts deleted file mode 100644 index 3a70a5eaae1c..000000000000 --- a/packages/angular/cli/src/utilities/markdown-loader.ts +++ /dev/null @@ -1,35 +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 { readFileSync } from 'node:fs'; - -const LONG_DESCRIPTION_REGEXP = /[/\\]long-description\.md$/; - -function isCommandLongDescription(filePath: string | undefined): boolean { - return !!filePath && LONG_DESCRIPTION_REGEXP.test(filePath); -} - -// Register markdown extension hook for CommonJS execution -if (typeof require !== 'undefined' && require.extensions) { - const originalMdExtension = require.extensions['.md']; - require.extensions['.md'] = (module, filename) => { - if (isCommandLongDescription(filename)) { - module.exports = readFileSync(filename, 'utf8'); - - return; - } - - if (originalMdExtension) { - originalMdExtension(module, filename); - } else { - const err = new Error(`Cannot find module '${filename}'`); - (err as NodeJS.ErrnoException).code = 'MODULE_NOT_FOUND'; - throw err; - } - }; -} diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json index 82edb52e27d8..b48e12387746 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -38,12 +38,12 @@ }, "devDependencies": { "@angular-devkit/schematics": "workspace:*", - "@angular/common": "22.2.0-next.5", - "@angular/compiler": "22.2.0-next.5", - "@angular/core": "22.2.0-next.5", - "@angular/platform-browser": "22.2.0-next.5", - "@angular/platform-server": "22.2.0-next.5", - "@angular/router": "22.2.0-next.5", + "@angular/common": "22.2.0-rc.0", + "@angular/compiler": "22.2.0-rc.0", + "@angular/core": "22.2.0-rc.0", + "@angular/platform-browser": "22.2.0-rc.0", + "@angular/platform-server": "22.2.0-rc.0", + "@angular/router": "22.2.0-rc.0", "@schematics/angular": "workspace:*" }, "sideEffects": false, diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index d7b95edd650a..4f699d7fbac5 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -66,7 +66,7 @@ "devDependencies": { "@angular/ssr": "workspace:*", "browser-sync": "3.0.4", - "ng-packagr": "22.2.0-next.5", + "ng-packagr": "22.2.0-rc.0", "sass": "1.104.1", "undici": "8.10.2" }, diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 77aeab6646a1..928771337bb5 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -61,19 +61,24 @@ export class SchemaValidationException extends BaseException { } const messages = errors.map((err) => { + if (err.keyword === 'additionalProperties') { + const unknown = err.params?.additionalProperty; + // `parentSchema` is the schema that rejected the property, which ajv only attaches when + // the validator was created with `verbose: true`. A schema that declares no `properties` + // of its own, such as one using only `patternProperties`, has no options to offer. + const known = Object.keys(err.parentSchema?.properties ?? {}); + + return ( + `Unknown option "${unknown}"${err.instancePath ? ` at "${err.instancePath}"` : ''}.` + + (known.length ? ` Valid options are: ${known.join(', ')}.` : '') + ); + } + let message = `Data path ${JSON.stringify(err.instancePath)} ${err.message}`; - if (err.params) { - switch (err.keyword) { - case 'additionalProperties': - message += `(${err.params.additionalProperty})`; - break; - - case 'enum': - message += `. Allowed values are: ${(err.params.allowedValues as string[] | undefined) - ?.map((v) => `"${v}"`) - .join(', ')}`; - break; - } + if (err.keyword === 'enum' && err.params) { + message += `. Allowed values are: ${(err.params.allowedValues as string[] | undefined) + ?.map((v) => `"${v}"`) + .join(', ')}`; } return message + '.'; @@ -106,6 +111,8 @@ export class CoreSchemaRegistry implements SchemaRegistry { strict: false, loadSchema: (uri: string) => this._fetch(uri), passContext: true, + // Needed to list the valid options of the object an unknown option was found in. + verbose: true, }); ajvAddFormats(this._ajv); diff --git a/packages/angular_devkit/core/src/json/schema/registry_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts index 34e404a63b29..32c601154d16 100644 --- a/packages/angular_devkit/core/src/json/schema/registry_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -7,8 +7,10 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { JsonValue } from '../utils'; import { SchemaFormat } from './interface'; import { CoreSchemaRegistry, SchemaValidationException } from './registry'; +import { JsonSchema } from './schema'; import { addUndefinedDefaults } from './transforms'; describe('CoreSchemaRegistry', () => { @@ -340,4 +342,93 @@ describe('CoreSchemaRegistry', () => { expect(deprecatedMessages[1]).toBe('Option "bar" is deprecated.'); expect(result.success).toBe(true, result.errors); }); + + describe('error messages', () => { + async function messagesFor(schema: JsonSchema, data: JsonValue): Promise { + const registry = new CoreSchemaRegistry(); + const validator = await registry.compile(schema); + const result = await validator(data); + expect(result.success).toBe(false); + + return SchemaValidationException.createMessages(result.errors); + } + + it('names an unknown option and the options that are valid there', async () => { + const messages = await messagesFor( + { + properties: { version: { type: 'number' }, projects: { type: 'object' } }, + additionalProperties: false, + }, + { allowedCommonJsDependencies: [] }, + ); + + expect(messages).toEqual([ + 'Unknown option "allowedCommonJsDependencies". Valid options are: version, projects.', + ]); + }); + + it('points at the object an unknown option was found in', async () => { + const messages = await messagesFor( + { + properties: { + cli: { + type: 'object', + properties: { cache: { type: 'object' }, packageManager: { type: 'string' } }, + additionalProperties: false, + }, + }, + }, + { cli: { completion: true } }, + ); + + expect(messages).toEqual([ + 'Unknown option "completion" at "/cli". Valid options are: cache, packageManager.', + ]); + }); + + it('looks through a $ref for the valid options', async () => { + const messages = await messagesFor( + { + $ref: '#/definitions/global', + definitions: { + global: { + type: 'object', + properties: { cli: { type: 'object' }, schematics: { type: 'object' } }, + additionalProperties: false, + }, + }, + }, + { version: 1 }, + ); + + expect(messages).toEqual(['Unknown option "version". Valid options are: cli, schematics.']); + }); + + it('omits the valid options when the schema does not list any', async () => { + const messages = await messagesFor( + { + $ref: '#/definitions/cli', + definitions: { + cli: { + type: 'object', + patternProperties: { '^x-': { type: 'string' } }, + additionalProperties: false, + }, + }, + }, + { completion: true }, + ); + + expect(messages).toEqual(['Unknown option "completion".']); + }); + + it('leaves an error that is not about an unknown option alone', async () => { + const messages = await messagesFor( + { properties: { outputPath: { type: 'string' } } }, + { outputPath: 42 }, + ); + + expect(messages).toEqual(['Data path "/outputPath" must be string.']); + }); + }); }); diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index aaca4ed7005e..07e9660520f8 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -17,8 +17,8 @@ }, "devDependencies": { "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", - "@angular/compiler": "22.2.0-next.5", - "@angular/compiler-cli": "22.2.0-next.5", + "@angular/compiler": "22.2.0-rc.0", + "@angular/compiler-cli": "22.2.0-rc.0", "typescript": "6.0.3", "webpack": "5.111.0" } diff --git a/packages/schematics/angular/utility/latest-versions/package.json b/packages/schematics/angular/utility/latest-versions/package.json index 111c8296c202..75481eb2b23d 100644 --- a/packages/schematics/angular/utility/latest-versions/package.json +++ b/packages/schematics/angular/utility/latest-versions/package.json @@ -5,7 +5,7 @@ "dependencies": { "@types/express": "^5.0.1", "@types/jasmine": "~6.0.0", - "@types/node": "^20.17.19", + "@types/node": "^22.12.0", "browser-sync": "^3.0.0", "express": "^5.1.0", "istanbul-lib-instrument": "^6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4cb97b4c448..c0c4d347a5ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,8 +172,8 @@ importers: .: dependencies: '@angular/compiler-cli': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3) typescript: specifier: 6.0.3 version: 6.0.3 @@ -184,44 +184,44 @@ importers: built: true devDependencies: '@angular/animations': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) '@angular/cdk': - specifier: 22.2.0-next.4 - version: 22.2.0-next.4(164cec07f845c9f2a6f509bb1888cfce) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(32444abcc2be6e3bf2b6c059aa24d737) '@angular/common': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) '@angular/compiler': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5 + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0 '@angular/core': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) '@angular/forms': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) '@angular/localize': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3))(@angular/compiler@22.2.0-next.5) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3))(@angular/compiler@22.2.0-rc.0) '@angular/material': - specifier: 22.2.0-next.4 - version: 22.2.0-next.4(95bc9c8a06206acb56b97c62cbcd354e) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(25ddf4063a130bbf19862bd16a6ab5d4) '@angular/ng-dev': - specifier: https://github.com/angular/dev-infra-private-ng-dev-builds.git#c3d7ed184d71d390d3eacff85f4b2b3f16b82660 - version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/c3d7ed184d71d390d3eacff85f4b2b3f16b82660 + specifier: https://github.com/angular/dev-infra-private-ng-dev-builds.git#f5de34cabca7e005dc8102d3abb026d01a7a881a + version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/f5de34cabca7e005dc8102d3abb026d01a7a881a '@angular/platform-browser': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) '@angular/platform-server': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/compiler@22.2.0-next.5)(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/compiler@22.2.0-rc.0)(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) '@angular/router': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) '@angular/service-worker': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) '@babel/core': specifier: 8.0.5 version: 8.0.5 @@ -463,8 +463,8 @@ importers: specifier: 30.0.1 version: 30.0.1 ng-packagr: - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3) rxjs: specifier: 7.8.2 version: 7.8.2 @@ -578,8 +578,8 @@ importers: specifier: 4.9.1 version: 4.9.1(supports-color@11.0.0) ng-packagr: - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3) postcss: specifier: 8.5.28 version: 8.5.28 @@ -667,23 +667,23 @@ importers: specifier: workspace:* version: link:../../angular_devkit/schematics '@angular/common': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) '@angular/compiler': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5 + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0 '@angular/core': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) '@angular/platform-browser': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) '@angular/platform-server': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/compiler@22.2.0-next.5)(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/compiler@22.2.0-rc.0)(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) '@angular/router': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) '@schematics/angular': specifier: workspace:* version: link:../../schematics/angular @@ -866,8 +866,8 @@ importers: specifier: 3.0.4 version: 3.0.4(bufferutil@4.1.0)(debug@4.4.3(supports-color@11.0.0))(supports-color@11.0.0)(utf-8-validate@6.0.6) ng-packagr: - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3) sass: specifier: 1.104.1 version: 1.104.1 @@ -962,11 +962,11 @@ importers: specifier: workspace:0.0.0-PLACEHOLDER version: link:../../angular_devkit/core '@angular/compiler': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5 + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0 '@angular/compiler-cli': - specifier: 22.2.0-next.5 - version: 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3) + specifier: 22.2.0-rc.0 + version: 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1026,15 +1026,14 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@angular/animations@22.2.0-next.5': - resolution: {integrity: sha512-Q2ImjJNM14L+jKh5EvxDtooRf8tshZ+rlomtf24AI6Cxr7FelPXwPOZbYmHkPu0y5hqhey5+WNG5RQLRvKjiLQ==} + '@angular/animations@22.2.0-rc.0': + resolution: {integrity: sha512-MnkNstor6AH92M9ps7RZfcCyUo24SqBZU71soDgYC3aXFJhSYOtJFPKE1/KgrpVt416lTzbhHe/68kAZ3cH9MQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} - deprecated: '@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.' peerDependencies: - '@angular/core': 22.2.0-next.5 + '@angular/core': 22.2.0-rc.0 - '@angular/cdk@22.2.0-next.4': - resolution: {integrity: sha512-ZqORCqi7eGIXgiObGA1Lmf7+l+7VP5gp7/ge3cNHUOcJP6HRJlt4ISjUFT+j7t9FU1lkf6nNBSwEcIG2EI+EGw==} + '@angular/cdk@22.2.0-rc.0': + resolution: {integrity: sha512-fG3/uCItdRj+PSetdHOIcd72VUekKhdj/uazHuhjjE1mz7gJJx0WBzDk1CcGUYtfvq0WoYlv9Kq6rhnP2gBAdw==} peerDependencies: '@angular/common': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 '@angular/core': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 @@ -1042,33 +1041,33 @@ packages: '@angular/platform-browser': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/common@22.2.0-next.5': - resolution: {integrity: sha512-D11bjuSXSuIB+EQ04Ep1NyhbvuJHbZqKBkNzXDK0Mfkkbjfl9rN4YOs7vGNcXOm/pFXWSR4DR5IqQWeiiSlUBQ==} + '@angular/common@22.2.0-rc.0': + resolution: {integrity: sha512-j80c+FC/yZ2jz0d6w/kPwvEdMhaIol23gt/IcZxOigvElUywpNfm+g36XjM34/UNy6HDhew5ck7HE9DWYVXwsw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/core': 22.2.0-next.5 + '@angular/core': 22.2.0-rc.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@22.2.0-next.5': - resolution: {integrity: sha512-zuAYA4ZAYf1HY82nJRuo7Ft00YwAiRo/aZGuRSU4A+RlHROq57X1W8NyINdMpMPEyL2O14WgRG2/MlGDKnF49Q==} + '@angular/compiler-cli@22.2.0-rc.0': + resolution: {integrity: sha512-51yfbHuAd09YnH5tTLb9jIxzkqFTB8SAj6Gv2/LqR+xDq7AGaZgFiehQOa2pa9kDJ3wNHYbTROF7dHP+AXXj2w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} hasBin: true peerDependencies: - '@angular/compiler': 22.2.0-next.5 + '@angular/compiler': 22.2.0-rc.0 typescript: '>=6.0 <6.1' peerDependenciesMeta: typescript: optional: true - '@angular/compiler@22.2.0-next.5': - resolution: {integrity: sha512-EZ3O1e0aQDsYPX8jffsX7mNoyqWHOMqpzP4ULjt45JZkpAT/H/Ss7p8MFKnox60AFbvmEENm/srcnDJ5s9sTLA==} + '@angular/compiler@22.2.0-rc.0': + resolution: {integrity: sha512-vWavQPwkNQhie4/KKGycoQrGGPNcjKbn8VkmMiIcfPVCMtvNvhs8ssPgKENYDXaFRyg+UtJ1Y/2HRdYtIQIw8g==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} - '@angular/core@22.2.0-next.5': - resolution: {integrity: sha512-Gh+JycsZiZhVZEODpCBEc20XXl6aIN/w+bB7A9a7/d0bMRdX3IBtYNxCOMor9L7bJVfwD+N3NTWpb+wSv81wMQ==} + '@angular/core@22.2.0-rc.0': + resolution: {integrity: sha512-HbeWk0EatMmOf5VF71W35WCVZ9nwjHkeXvdhee+1uZfr2cmpnS3OXuFrIgTP2kSrILc6jEh7xuHW/IT1itjHuA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/compiler': 22.2.0-next.5 + '@angular/compiler': 22.2.0-rc.0 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 || ~0.16.0 peerDependenciesMeta: @@ -1077,74 +1076,74 @@ packages: zone.js: optional: true - '@angular/forms@22.2.0-next.5': - resolution: {integrity: sha512-eyECVvZ/pJGKzXTWf3SC1d+hSIVWQvJreiCo0kX2e9pDzuGoK5Br8gifP38tgwym2FuR/5THJRn2K998pUZ4Xg==} + '@angular/forms@22.2.0-rc.0': + resolution: {integrity: sha512-mg+NratvkCH9ihGBplqZBKS2Q8+eLDdZSyl10FWmIzhaTmJEk9GLoZ70n4kEU0rX65hy2oF0M2aTBpr2CRxZ4w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.2.0-next.5 - '@angular/core': 22.2.0-next.5 - '@angular/platform-browser': 22.2.0-next.5 + '@angular/common': 22.2.0-rc.0 + '@angular/core': 22.2.0-rc.0 + '@angular/platform-browser': 22.2.0-rc.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/localize@22.2.0-next.5': - resolution: {integrity: sha512-kquo2tv5g3SF33acN6SQpiOwr0TpP45/iKRLkX/xcWY6XZ7S9bxU4W6CiJ4fZfelfTDKBr6ZhxA8QNnw7Y1Z8w==} + '@angular/localize@22.2.0-rc.0': + resolution: {integrity: sha512-c0WgdyX0GYrx4hBF9KEPaOXFIcz+j9dIgLDG2hcsxLdjE9r+trmTBFwhR/4/Q7S5GThbnty4/rjVJOrRXpdi7Q==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} hasBin: true peerDependencies: - '@angular/compiler': 22.2.0-next.5 - '@angular/compiler-cli': 22.2.0-next.5 + '@angular/compiler': 22.2.0-rc.0 + '@angular/compiler-cli': 22.2.0-rc.0 - '@angular/material@22.2.0-next.4': - resolution: {integrity: sha512-BTGnryNFfLvN8jI02msMNbHdNVcOaFL0MMH8S+jpQc8ooULx8bRwQZSWk8LKusayVyZH4JbT3YvV7PW/qw0ndA==} + '@angular/material@22.2.0-rc.0': + resolution: {integrity: sha512-3IaSPe7LnbhJHzo95xAfY0vB7nA5tDxx+ojn8MSZTa/m6Cw8aCRMZiSiJ783Qw7x1doWz37o3Z7B54Wp8n/2Jw==} peerDependencies: - '@angular/cdk': 22.2.0-next.4 + '@angular/cdk': 22.2.0-rc.0 '@angular/common': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 '@angular/core': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 '@angular/forms': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 '@angular/platform-browser': ^22.0.0-0 || ^22.1.0-0 || ^22.2.0-0 || ^22.3.0-0 || ^23.0.0-0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/c3d7ed184d71d390d3eacff85f4b2b3f16b82660': - resolution: {gitHosted: true, integrity: sha512-DBHbT5IV4wnEUd5/+wh1cTxmwKNsxGahmN4DJ92T1aY5DcvCxhsoedLtHNX9Wi81hNEc37phIr+qaDsZWQVdQw==, tarball: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/c3d7ed184d71d390d3eacff85f4b2b3f16b82660} - version: 0.0.0-f5c817076b8e4da7b6e91783dbcd553c5003a296 + '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/f5de34cabca7e005dc8102d3abb026d01a7a881a': + resolution: {gitHosted: true, integrity: sha512-JEXOlRtQ50aV/E2C+PeGKYCmGHWOi6G70kiS0tTjKGAn+qrYTRQQzFSm4YB2VVYnL16s3jg8oAIwZ4pl/t9WgQ==, tarball: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/f5de34cabca7e005dc8102d3abb026d01a7a881a} + version: 0.0.0-71e60c68d9d483b0d763b8e6ed4dd9deb8da3b1a hasBin: true - '@angular/platform-browser@22.2.0-next.5': - resolution: {integrity: sha512-eSTpC4/SqkQ6QDTblFNznT7jRwX9o4X1cO7Hf+F0p07hofbbi8ptDXl0iZs6643C5eSaoyp1Bncd8wJzwoFmPw==} + '@angular/platform-browser@22.2.0-rc.0': + resolution: {integrity: sha512-poN1dCkFKM1t288v/5M2w+OUzragcUuEt3GQfcQQ87lIWCp5atCpClpXxbFJj0VUeYXPKvb9rUwQtFtTMzYohA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/animations': 22.2.0-next.5 - '@angular/common': 22.2.0-next.5 - '@angular/core': 22.2.0-next.5 + '@angular/animations': 22.2.0-rc.0 + '@angular/common': 22.2.0-rc.0 + '@angular/core': 22.2.0-rc.0 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/platform-server@22.2.0-next.5': - resolution: {integrity: sha512-MrXjF0GI2CSnNBbAhOJXjsXraL5H3P32VaG9Eh6DJNNZpR6pzM9HWOjbXHQ1elrYNBN2rV67HRSiDkRYu46AVw==} + '@angular/platform-server@22.2.0-rc.0': + resolution: {integrity: sha512-bohsXRaIV/d/RqFqbsdpmNpmcmurSrV/a1I7lo7IEzJJXAVchHBD/JIjJYFOk+NgqLDYZciq6Z8fF8idLfv1YQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.2.0-next.5 - '@angular/compiler': 22.2.0-next.5 - '@angular/core': 22.2.0-next.5 - '@angular/platform-browser': 22.2.0-next.5 + '@angular/common': 22.2.0-rc.0 + '@angular/compiler': 22.2.0-rc.0 + '@angular/core': 22.2.0-rc.0 + '@angular/platform-browser': 22.2.0-rc.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/router@22.2.0-next.5': - resolution: {integrity: sha512-40viSK016CMgwVjbW8oLLv0zZSWCNVCFG1bqxOnqujPbP/lW3vlu4ul0Ine9tUlKnkfNZKV+NTCQtHlY1li+5Q==} + '@angular/router@22.2.0-rc.0': + resolution: {integrity: sha512-bIOOdp5T3KM4vB2xL4L6fpXYQ3vJmkPRB+k2tXSb4xW1HLsJDEyGN9GNj/5prnLbaWIGqcfhGNwnTW/mCVm42A==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.2.0-next.5 - '@angular/core': 22.2.0-next.5 - '@angular/platform-browser': 22.2.0-next.5 + '@angular/common': 22.2.0-rc.0 + '@angular/core': 22.2.0-rc.0 + '@angular/platform-browser': 22.2.0-rc.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/service-worker@22.2.0-next.5': - resolution: {integrity: sha512-z8oExlAiu3oKkKQFZ0AXnmk1K9jw/+lLEv9IRhuQM1abPNKfFo9AHE298dPZLoE3wqumg85No2u8AJJX9w/1sw==} + '@angular/service-worker@22.2.0-rc.0': + resolution: {integrity: sha512-y03a1Ppfyi765n8QlRgVqeL5kF8k5mFOVxvK+ZlxONave1mN2ChG37Yu35gzJ22wiprBDUgxGm3UdLvscU3YkA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} hasBin: true peerDependencies: - '@angular/core': 22.2.0-next.5 + '@angular/core': 22.2.0-rc.0 rxjs: ^6.5.3 || ^7.4.0 '@asamuzakjp/css-color@6.0.7': @@ -1187,10 +1186,6 @@ packages: resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} - '@babel/generator@8.0.0': - resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/generator@8.0.5': resolution: {integrity: sha512-f/TuhuMAxJqhwxEGNsJrswuG9VHmh0oNFoQoo6TbpgtFAz9wYZXcTAcWZMHfp7ljesr0RG04bp3Aos9GI59L7w==} engines: {node: ^22.18.0 || >=24.11.0} @@ -3270,26 +3265,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pnpm/crypto.hash@1000.2.2': - resolution: {integrity: sha512-W8pLZvXWLlGG5p0Z2nCvtBhlM6uuTcbAbsS15wlGS31jBBJKJW2udLoFeM7qfWPo7E2PqRPGxca7APpVYAjJhw==} - engines: {node: '>=18.12'} - - '@pnpm/crypto.polyfill@1000.1.0': - resolution: {integrity: sha512-tNe7a6U4rCpxLMBaR0SIYTdjxGdL0Vwb3G1zY8++sPtHSvy7qd54u8CIB0Z+Y6t5tc9pNYMYCMwhE/wdSY7ltg==} - engines: {node: '>=18.12'} - - '@pnpm/dependency-path@1001.1.10': - resolution: {integrity: sha512-PNImtV2SmNTDpLi4HdN86tJPmsOeIxm4VhmxgBVsMrJPEBfkNEWFcflR3wU6XVn/26g9qWdvlNHaawtCjeB93Q==} - engines: {node: '>=18.12'} - - '@pnpm/graceful-fs@1000.1.0': - resolution: {integrity: sha512-EsMX4slK0qJN2AR0/AYohY5m0HQNYGMNe+jhN74O994zp22/WbX+PbkIKyw3UQn39yQm2+z6SgwklDxbeapsmQ==} - engines: {node: '>=18.12'} - - '@pnpm/types@1001.3.0': - resolution: {integrity: sha512-NLTXheat/u7OEGg5M5vF6Z85zx8uKUZE0+whtX/sbFV2XL48RdnOWGPTKYuVVkv8M+launaLUTgGEXNs/ess2w==} - engines: {node: '>=18.12'} - '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -6768,8 +6743,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - ng-packagr@22.2.0-next.5: - resolution: {integrity: sha512-fZixAT5MGDlxZnPISy1ml22DeOFVMwt3Ym/6l/VwzuAlj1K0YVJNgxxRQs+iX/qqDzcXjyVD+CQAAwAq5pQo3Q==} + ng-packagr@22.2.0-rc.0: + resolution: {integrity: sha512-l91WqbCFBKpVmDwW/rttDNe6kpRjsIlHnUf55YTH5JqN0Q7uVGBy6QbRUV2S+4/StyFqc+keOAtVPJUdO2bvxw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} hasBin: true peerDependencies: @@ -7946,10 +7921,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - ssri@10.0.5: - resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -8794,9 +8765,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.5.4: - resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} - zod@4.6.5: resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} @@ -8826,30 +8794,30 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))': + '@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))': dependencies: - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) tslib: 2.8.1 - '@angular/cdk@22.2.0-next.4(164cec07f845c9f2a6f509bb1888cfce)': + '@angular/cdk@22.2.0-rc.0(32444abcc2be6e3bf2b6c059aa24d737)': dependencies: - '@angular/common': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/forms': 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) - '@angular/platform-browser': 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/common': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/forms': 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + '@angular/platform-browser': 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) parse5: 8.0.1 rxjs: 7.8.2 tslib: 2.8.1 - '@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2)': + '@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2)': dependencies: - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3)': + '@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3)': dependencies: - '@angular/compiler': 22.2.0-next.5 + '@angular/compiler': 22.2.0-rc.0 '@babel/core': 8.0.1 '@jridgewell/sourcemap-codec': 1.6.0 chokidar: 5.0.0 @@ -8861,47 +8829,47 @@ snapshots: optionalDependencies: typescript: 6.0.3 - '@angular/compiler@22.2.0-next.5': + '@angular/compiler@22.2.0-rc.0': dependencies: tslib: 2.8.1 - '@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)': + '@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)': dependencies: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@angular/compiler': 22.2.0-next.5 + '@angular/compiler': 22.2.0-rc.0 zone.js: 0.16.3 - '@angular/forms@22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': + '@angular/forms@22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/platform-browser': 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/common': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/platform-browser': 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) '@standard-schema/spec': 1.1.0 rxjs: 7.8.2 tslib: 2.8.1 - zod: 4.5.4 + zod: 4.6.5 - '@angular/localize@22.2.0-next.5(@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3))(@angular/compiler@22.2.0-next.5)': + '@angular/localize@22.2.0-rc.0(@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3))(@angular/compiler@22.2.0-rc.0)': dependencies: - '@angular/compiler': 22.2.0-next.5 - '@angular/compiler-cli': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3) + '@angular/compiler': 22.2.0-rc.0 + '@angular/compiler-cli': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3) '@babel/core': 8.0.1 tinyglobby: 0.2.17 yargs: 18.1.0 - '@angular/material@22.2.0-next.4(95bc9c8a06206acb56b97c62cbcd354e)': + '@angular/material@22.2.0-rc.0(25ddf4063a130bbf19862bd16a6ab5d4)': dependencies: - '@angular/cdk': 22.2.0-next.4(164cec07f845c9f2a6f509bb1888cfce) - '@angular/common': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/forms': 22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) - '@angular/platform-browser': 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/cdk': 22.2.0-rc.0(32444abcc2be6e3bf2b6c059aa24d737) + '@angular/common': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/forms': 22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + '@angular/platform-browser': 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/c3d7ed184d71d390d3eacff85f4b2b3f16b82660': + '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/f5de34cabca7e005dc8102d3abb026d01a7a881a': dependencies: '@actions/core': 3.0.1 '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2) @@ -8919,7 +8887,6 @@ snapshots: '@octokit/request-error': 7.1.2 '@octokit/rest': 22.0.1 '@octokit/types': 17.0.0 - '@pnpm/dependency-path': 1001.1.10 '@types/cli-progress': 3.11.6 '@types/ejs': 3.1.5 '@types/events': 3.0.3 @@ -8956,40 +8923,40 @@ snapshots: which: 7.0.0 yaml: 2.9.0 yargs: 18.1.0 - zod: 4.5.4 + zod: 4.6.5 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - '@react-native-async-storage/async-storage' - '@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))': + '@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))': dependencies: - '@angular/common': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/common': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) tslib: 2.8.1 optionalDependencies: - '@angular/animations': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/animations': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) - '@angular/platform-server@22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/compiler@22.2.0-next.5)(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': + '@angular/platform-server@22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/compiler@22.2.0-rc.0)(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/compiler': 22.2.0-next.5 - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/platform-browser': 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/common': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/compiler': 22.2.0-rc.0 + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/platform-browser': 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) rxjs: 7.8.2 tslib: 2.8.1 xhr2: 0.2.1 - '@angular/router@22.2.0-next.5(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': + '@angular/router@22.2.0-rc.0(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/platform-browser': 22.2.0-next.5(@angular/animations@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/common': 22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/platform-browser': 22.2.0-rc.0(@angular/animations@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@angular/common@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/service-worker@22.2.0-next.5(@angular/core@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2)': + '@angular/service-worker@22.2.0-rc.0(@angular/core@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2)': dependencies: - '@angular/core': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/core': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(rxjs@7.8.2)(zone.js@0.16.3) rxjs: 7.8.2 tslib: 2.8.1 @@ -9046,7 +9013,7 @@ snapshots: '@babel/core@8.0.1': dependencies: '@babel/code-frame': 8.0.0 - '@babel/generator': 8.0.0 + '@babel/generator': 8.0.5 '@babel/helper-compilation-targets': 8.0.5 '@babel/helpers': 8.0.5 '@babel/parser': 8.0.5 @@ -9089,15 +9056,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/generator@8.0.0': - dependencies: - '@babel/parser': 8.0.5 - '@babel/types': 8.0.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@types/jsesc': 2.5.1 - jsesc: 3.1.0 - '@babel/generator@8.0.5': dependencies: '@babel/parser': 8.0.5 @@ -11304,26 +11262,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pnpm/crypto.hash@1000.2.2': - dependencies: - '@pnpm/crypto.polyfill': 1000.1.0 - '@pnpm/graceful-fs': 1000.1.0 - ssri: 10.0.5 - - '@pnpm/crypto.polyfill@1000.1.0': {} - - '@pnpm/dependency-path@1001.1.10': - dependencies: - '@pnpm/crypto.hash': 1000.2.2 - '@pnpm/types': 1001.3.0 - semver: 7.8.5 - - '@pnpm/graceful-fs@1000.1.0': - dependencies: - graceful-fs: 4.2.11 - - '@pnpm/types@1001.3.0': {} - '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -15129,10 +15067,10 @@ snapshots: neo-async@2.6.2: {} - ng-packagr@22.2.0-next.5(@angular/compiler-cli@22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3): + ng-packagr@22.2.0-rc.0(@angular/compiler-cli@22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3))(supports-color@11.0.0)(tslib@2.8.1)(typescript@6.0.3): dependencies: '@ampproject/remapping': 2.3.0 - '@angular/compiler-cli': 22.2.0-next.5(@angular/compiler@22.2.0-next.5)(typescript@6.0.3) + '@angular/compiler-cli': 22.2.0-rc.0(@angular/compiler@22.2.0-rc.0)(typescript@6.0.3) ajv: 8.20.0 browserslist: 4.28.9 chokidar: 5.0.0 @@ -15148,8 +15086,8 @@ snapshots: rolldown: 1.2.8 rolldown-plugin-dts: 0.28.5(rolldown@1.2.8)(typescript@6.0.3) rxjs: 7.8.2 - sass: 1.104.0 - sass-embedded: 1.104.0 + sass: 1.104.1 + sass-embedded: 1.104.1 tinyglobby: 0.2.17 tslib: 2.8.1 typescript: 6.0.3 @@ -16158,6 +16096,7 @@ snapshots: sass-embedded-unknown-all: 1.104.0 sass-embedded-win32-arm64: 1.104.0 sass-embedded-win32-x64: 1.104.0 + optional: true sass-embedded@1.104.1: dependencies: @@ -16201,6 +16140,7 @@ snapshots: source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.6.0 + optional: true sass@1.104.1: dependencies: @@ -16467,10 +16407,6 @@ snapshots: safer-buffer: 2.1.2 tweetnacl: 0.14.5 - ssri@10.0.5: - dependencies: - minipass: 7.1.3 - stack-trace@0.0.10: {} stackback@0.0.2: {} @@ -17482,8 +17418,6 @@ snapshots: zod@3.25.76: {} - zod@4.5.4: {} - zod@4.6.5: {} zone.js@0.16.3: {} diff --git a/scripts/benchmark.mts b/scripts/benchmark.mts new file mode 100644 index 000000000000..bf35b97808e3 --- /dev/null +++ b/scripts/benchmark.mts @@ -0,0 +1,150 @@ +/** + * @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 fs from 'node:fs'; +import path from 'node:path'; +import { type BenchmarkCliOptions, runI18nBenchmarks } from './benchmarks/i18n/index.mts'; + +function checkBuildStatus(logger: Console): boolean { + const distFile = 'dist/@angular/build/src/tools/esbuild/i18n-inliner.js'; + const srcFile = 'packages/angular/build/src/tools/esbuild/i18n-inliner.ts'; + + if (!fs.existsSync(distFile)) { + logger.error( + 'Error: @angular/build has not been built yet.\nPlease run "pnpm build" before benchmarking.', + ); + + return false; + } + + if (fs.existsSync(srcFile)) { + const srcMtime = fs.statSync(srcFile).mtimeMs; + const distMtime = fs.statSync(distFile).mtimeMs; + if (srcMtime > distMtime) { + logger.warn( + 'Warning: Source files in packages/angular/build are newer than dist/.\n' + + 'Run "pnpm build" to ensure your benchmark reflects your latest local edits.\n', + ); + } + } + + return true; +} + +export default async function ( + options: { + _?: string[]; + scenario?: string; + iterations?: string | number; + warmup?: string | number; + concurrency?: string | number; + build?: boolean; + json?: boolean; + inProcess?: boolean; + 'in-process'?: boolean; + saveBaseline?: string; + 'save-baseline'?: string; + compareBaseline?: string; + 'compare-baseline'?: string; + help?: boolean; + [key: string]: unknown; + }, + _cwd: string, +): Promise { + const positionals = options._ ?? []; + const targetSubsystem = positionals[0] ?? 'i18n'; + + if (options.help || targetSubsystem === 'help') { + // eslint-disable-next-line no-console + console.log(` +Angular CLI Performance Benchmark Runner + +Usage: + pnpm admin benchmark [subsystem] [options] + +Subsystems: + i18n Run i18n inliner performance benchmarks (default) + +Options: + --scenario= Run a specific scenario (e.g. standard-app, enterprise-multilingual) + --iterations= Number of measured iterations (default: 5) + --warmup= Number of warmup iterations (default: 2) + --concurrency= Override worker thread pool concurrency + --in-process Run all scenarios in a single process (useful for debugging) + --build Automatically build packages before benchmarking + --json Output results in machine-readable JSON + --save-baseline= Save run results to a baseline JSON file + --compare-baseline= Compare run results against an existing baseline JSON file + --help Show this help message +`); + + return 0; + } + + if (targetSubsystem !== 'i18n') { + // eslint-disable-next-line no-console + console.error(`Unknown benchmark subsystem: "${targetSubsystem}". Supported subsystems: i18n`); + + return 1; + } + + if (options.build) { + const buildModule = await import('./build.mts'); + await buildModule.default({ local: true }); + } + + // eslint-disable-next-line no-console + if (!checkBuildStatus(console)) { + return 1; + } + + const rawSaveBaseline = options.saveBaseline ?? options['save-baseline']; + const rawCompareBaseline = options.compareBaseline ?? options['compare-baseline']; + + const iterations = options.iterations !== undefined ? Number(options.iterations) : undefined; + const warmup = options.warmup !== undefined ? Number(options.warmup) : undefined; + const concurrency = options.concurrency !== undefined ? Number(options.concurrency) : undefined; + + if (iterations !== undefined && (!Number.isInteger(iterations) || iterations < 1)) { + // eslint-disable-next-line no-console + console.error('Error: --iterations must be a positive integer.'); + + return 1; + } + + if (warmup !== undefined && (!Number.isInteger(warmup) || warmup < 0)) { + // eslint-disable-next-line no-console + console.error('Error: --warmup must be a non-negative integer.'); + + return 1; + } + + if (concurrency !== undefined && (!Number.isInteger(concurrency) || concurrency < 1)) { + // eslint-disable-next-line no-console + console.error('Error: --concurrency must be a positive integer.'); + + return 1; + } + + const cliOptions: BenchmarkCliOptions = { + scenario: options.scenario, + iterations, + warmup, + concurrency, + json: Boolean(options.json), + inProcess: Boolean(options.inProcess ?? options['in-process']), + saveBaseline: rawSaveBaseline ? path.resolve(_cwd, String(rawSaveBaseline)) : undefined, + compareBaseline: rawCompareBaseline + ? path.resolve(_cwd, String(rawCompareBaseline)) + : undefined, + }; + + const { exitCode } = await runI18nBenchmarks(cliOptions); + + return exitCode; +} diff --git a/scripts/benchmarks/i18n/fixtures.mts b/scripts/benchmarks/i18n/fixtures.mts new file mode 100644 index 000000000000..3d53c3dbff5b --- /dev/null +++ b/scripts/benchmarks/i18n/fixtures.mts @@ -0,0 +1,165 @@ +/** + * @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 './init-env.mts'; +import type { ɵParsedTranslation } from '@angular/localize'; +import { transformSync } from 'esbuild'; +import { createRequire } from 'node:module'; + +import path from 'node:path'; + +import type { BuildOutputFile } from '../../../packages/angular/build/src/tools/esbuild/bundler-files.js'; +import type { LocaleInlineOptions } from '../../../packages/angular/build/src/tools/esbuild/i18n-inliner.js'; + +// Setup module paths to resolve dependencies from packages/angular/build +const requireFromBuild = createRequire( + path.resolve(import.meta.dirname, '../../../packages/angular/build/package.json'), +); + +const { BuildOutputFileType, createOutputFile } = requireFromBuild( + '../../../dist/@angular/build/src/tools/esbuild/bundler-files.js', +) as typeof import('../../../packages/angular/build/src/tools/esbuild/bundler-files.js'); + +const { calculateHash, initializeHash } = requireFromBuild( + '../../../dist/@angular/build/src/utils/hash.js', +) as typeof import('../../../packages/angular/build/src/utils/hash.js'); + +let isHashInitialized = false; + +export async function initializeFixtures(): Promise { + if (!isHashInitialized) { + await initializeHash(); + isHashInitialized = true; + } +} + +export function parsedTranslation( + parts: string[], + placeholderNames: string[] = [], + text?: string, +): ɵParsedTranslation { + return { + messageParts: Object.assign([...parts], { raw: [...parts] }), + placeholderNames, + text: text ?? parts.join(''), + }; +} + +export function generateTranslations( + locales: string[], + messageCount: number, +): LocaleInlineOptions[] { + return locales.map((locale) => { + const translation: Record = {}; + + for (let i = 0; i < messageCount; i++) { + const msgId = `msg_${i}`; + translation[msgId] = parsedTranslation( + [`[${locale}] Order #`, ` was confirmed for customer `, `. Thank you!`], + ['orderId', 'customerName'], + `[${locale}] Order #${i} was confirmed for customer Doe. Thank you!`, + ); + } + + const translationIntegrity = calculateHash(JSON.stringify(translation)); + + return { + locale, + translation, + translationIntegrity, + }; + }); +} + +export interface SyntheticBundleOptions { + filename: string; + targetByteSize: number; + messageCount: number; + withSourceMap?: boolean; + messageIdOffset?: number; +} + +export function generateSyntheticBundle(options: SyntheticBundleOptions): { + codeFile: BuildOutputFile; + mapFile?: BuildOutputFile; +} { + const { + filename, + targetByteSize, + messageCount, + withSourceMap = true, + messageIdOffset = 0, + } = options; + + const parts: string[] = [ + '// Synthetic test bundle generated for i18n-inliner benchmark\n', + 'export const BUNDLE_META = { generated: true, timestamp: Date.now() };\n', + ]; + + // Generate functions with $localize call sites + for (let i = 0; i < messageCount; i++) { + const msgId = `msg_${messageIdOffset + i}`; + parts.push( + `export function renderMessage_${i}(orderId, customerName) {\n`, + ` return $localize\`:@@${msgId}:Order #\${orderId}:orderId: ` + + `was confirmed for customer \${customerName}:customerName:. Thank you!\`;\n`, + `}\n`, + ); + } + + // Calculate current approximate size and pad with realistic JS functions if needed + let currentSize = parts.reduce((acc, str) => acc + str.length, 0); + let classIndex = 0; + + while (currentSize < targetByteSize) { + const filler = + `export class DataProcessor_${classIndex} {\n` + + ` constructor(id, options = {}) {\n` + + ` this.id = id;\n` + + ` this.options = Object.assign({ enabled: true, retries: 3 }, options);\n` + + ` this.history = [];\n` + + ` }\n` + + ` process(batch) {\n` + + ` if (!Array.isArray(batch)) return [];\n` + + ` const result = batch.map((item, idx) => ({\n` + + ` id: this.id + '_' + idx,\n` + + ` source: item,\n` + + ` timestamp: Date.now(),\n` + + ` active: true\n` + + ` }));\n` + + ` this.history.push(...result);\n` + + ` return result;\n` + + ` }\n` + + `}\n`; + + parts.push(filler); + currentSize += filler.length; + classIndex++; + } + + const rawCode = parts.join(''); + + let finalCode = rawCode; + let finalMap: string | undefined; + + if (withSourceMap) { + const result = transformSync(rawCode, { + sourcemap: true, + sourcefile: filename.replace(/\.js$/, '.ts'), + }); + finalCode = result.code; + finalMap = result.map; + } + + const codeFile = createOutputFile(filename, finalCode, BuildOutputFileType.Browser); + const mapFile = finalMap + ? createOutputFile(filename + '.map', finalMap, BuildOutputFileType.Browser) + : undefined; + + return { codeFile, mapFile }; +} diff --git a/scripts/benchmarks/i18n/harness.mts b/scripts/benchmarks/i18n/harness.mts new file mode 100644 index 000000000000..caf7f0753d24 --- /dev/null +++ b/scripts/benchmarks/i18n/harness.mts @@ -0,0 +1,155 @@ +/** + * @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 './init-env.mts'; + +export interface BenchmarkScenario { + name: string; + description: string; + inputSizeBytes: number; + localeCount: number; + run(iteration: number): Promise; + setup?(): Promise; + teardown?(): Promise; +} + +export interface ScenarioResult { + name: string; + description: string; + inputSizeBytes: number; + localeCount: number; + iterations: number; + warmup: number; + durationsMs: number[]; + minMs: number; + maxMs: number; + meanMs: number; + medianMs: number; + p95Ms: number; + stdDevMs: number; + throughputMBps: number; + peakRssBytes: number; + peakHeapBytes: number; + rssDeltaBytes: number; + heapUsedDeltaBytes: number; +} + +export interface BenchmarkRunOptions { + warmup?: number; + iterations?: number; +} + +function calculatePercentile(sortedValues: number[], percentile: number): number { + if (sortedValues.length === 0) { + return 0; + } + const index = (percentile / 100) * (sortedValues.length - 1); + const lower = Math.floor(index); + const upper = Math.ceil(index); + const weight = index - lower; + + return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight; +} + +export async function runScenario( + scenario: BenchmarkScenario, + options: BenchmarkRunOptions = {}, +): Promise { + const warmup = options.warmup ?? 2; + const iterations = options.iterations ?? 5; + + try { + await scenario.setup?.(); + + // Warmup phase + for (let w = 0; w < warmup; w++) { + // Force GC if available between warmups + global.gc?.(); + await scenario.run(-(w + 1)); + } + + // Measurement phase + const durationsMs: number[] = []; + let peakRssBytes = 0; + let peakHeapBytes = 0; + let maxRssDeltaBytes = 0; + let initialHeap = 0; + let finalHeap = 0; + + for (let i = 0; i < iterations; i++) { + global.gc?.(); + + const memBefore = process.memoryUsage(); + if (i === 0) { + initialHeap = memBefore.heapUsed; + } + + const start = performance.now(); + await scenario.run(i); + const duration = performance.now() - start; + + durationsMs.push(duration); + + const memAfter = process.memoryUsage(); + if (memAfter.rss > peakRssBytes) { + peakRssBytes = memAfter.rss; + } + if (memAfter.heapUsed > peakHeapBytes) { + peakHeapBytes = memAfter.heapUsed; + } + const rssDelta = Math.max(0, memAfter.rss - memBefore.rss); + if (rssDelta > maxRssDeltaBytes) { + maxRssDeltaBytes = rssDelta; + } + + // Force GC immediately after iteration to clean main thread isolate + global.gc?.(); + finalHeap = process.memoryUsage().heapUsed; + } + + // Sort ascending for percentile computation + const sorted = [...durationsMs].sort((a, b) => a - b); + const minMs = sorted[0]; + const maxMs = sorted[sorted.length - 1]; + const meanMs = durationsMs.reduce((sum, d) => sum + d, 0) / durationsMs.length; + const medianMs = calculatePercentile(sorted, 50); + const p95Ms = calculatePercentile(sorted, 95); + + const variance = + durationsMs.reduce((sum, d) => sum + Math.pow(d - meanMs, 2), 0) / durationsMs.length; + const stdDevMs = Math.sqrt(variance); + + // Throughput: total effective processed code volume in MB / mean seconds + const totalProcessedMb = (scenario.inputSizeBytes * scenario.localeCount) / (1024 * 1024); + const meanSeconds = meanMs / 1000; + const throughputMBps = meanSeconds > 0 ? totalProcessedMb / meanSeconds : 0; + + return { + name: scenario.name, + description: scenario.description, + inputSizeBytes: scenario.inputSizeBytes, + localeCount: scenario.localeCount, + iterations, + warmup, + durationsMs, + minMs, + maxMs, + meanMs, + medianMs, + p95Ms, + stdDevMs, + throughputMBps, + peakRssBytes, + peakHeapBytes, + rssDeltaBytes: maxRssDeltaBytes, + heapUsedDeltaBytes: Math.max(0, finalHeap - initialHeap), + }; + } finally { + await scenario.teardown?.(); + } +} diff --git a/scripts/benchmarks/i18n/index.mts b/scripts/benchmarks/i18n/index.mts new file mode 100644 index 000000000000..276671c193e0 --- /dev/null +++ b/scripts/benchmarks/i18n/index.mts @@ -0,0 +1,167 @@ +/** + * @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 './init-env.mts'; + +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { type ScenarioResult, runScenario } from './harness.mts'; +import { + buildReportData, + formatComparisonTable, + formatConsoleTable, + formatJsonReport, +} from './reporters.mts'; +import { type ScenarioFactoryOptions, getAllScenarios, getScenarioByName } from './scenarios.mts'; + +export interface BenchmarkCliOptions extends ScenarioFactoryOptions { + scenario?: string; + iterations?: number; + warmup?: number; + verbose?: boolean; + json?: boolean; + inProcess?: boolean; + saveBaseline?: string; + compareBaseline?: string; +} + +export async function runI18nBenchmarks( + options: BenchmarkCliOptions = {}, +): Promise<{ results: ScenarioResult[]; exitCode: number }> { + const warmup = options.warmup ?? 2; + const iterations = options.iterations ?? 5; + + let scenariosToRun = getAllScenarios({ concurrency: options.concurrency }); + + if (options.scenario) { + const single = getScenarioByName(options.scenario, { concurrency: options.concurrency }); + if (!single) { + // eslint-disable-next-line no-console + console.error( + `Unknown scenario: "${options.scenario}".\nAvailable scenarios: ${scenariosToRun.map((s) => s.name).join(', ')}`, + ); + + return { results: [], exitCode: 1 }; + } + scenariosToRun = [single]; + } + + const results: ScenarioResult[] = []; + + // When running multiple scenarios in a suite, isolate each scenario into its own child process + // so operating system memory and thread caches are not accumulated across scenarios. + if (scenariosToRun.length > 1 && !options.inProcess) { + for (const scenario of scenariosToRun) { + if (!options.json) { + // eslint-disable-next-line no-console + console.log( + `Running scenario: ${scenario.name} (${warmup} warmups, ${iterations} iterations)...`, + ); + } + + const args = [ + '--no-warnings=ExperimentalWarning', + '--experimental-transform-types', + '--expose-gc', + path.resolve(import.meta.dirname, '../../devkit-admin.mts'), + 'benchmark', + 'i18n', + `--scenario=${scenario.name}`, + `--warmup=${warmup}`, + `--iterations=${iterations}`, + '--json', + ]; + if (options.concurrency !== undefined) { + args.push(`--concurrency=${options.concurrency}`); + } + + const proc = spawnSync(process.execPath, args, { encoding: 'utf-8' }); + if (proc.status !== 0 || proc.error) { + // eslint-disable-next-line no-console + console.error( + `Error running scenario ${scenario.name}:\n` + + (proc.error?.message ?? (proc.stderr || proc.stdout)), + ); + + return { results, exitCode: 1 }; + } + + try { + const parsed = JSON.parse(proc.stdout); + const scenarioResult: ScenarioResult | undefined = Array.isArray(parsed) + ? parsed[0] + : parsed.results?.[0]; + if (scenarioResult) { + results.push(scenarioResult); + } + } catch { + // eslint-disable-next-line no-console + console.error(`Failed to parse result for scenario ${scenario.name}:\n${proc.stdout}`); + + return { results, exitCode: 1 }; + } + } + } else { + for (const scenario of scenariosToRun) { + if (!options.json) { + // eslint-disable-next-line no-console + console.log( + `Running scenario: ${scenario.name} (${warmup} warmups, ${iterations} iterations)...`, + ); + } + + try { + const result = await runScenario(scenario, { warmup, iterations }); + results.push(result); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Error running scenario ${scenario.name}:`, error); + + return { results, exitCode: 1 }; + } + } + } + + if (options.json) { + // eslint-disable-next-line no-console + console.log(formatJsonReport(results)); + } else { + // eslint-disable-next-line no-console + console.log('\n' + formatConsoleTable(results)); + } + + if (options.saveBaseline) { + const reportData = buildReportData(results); + await fs.writeFile(options.saveBaseline, JSON.stringify(reportData, null, 2), 'utf-8'); + if (!options.json) { + // eslint-disable-next-line no-console + console.log(`Saved baseline to: ${options.saveBaseline}`); + } + } + + if (options.compareBaseline) { + try { + const baselineContent = await fs.readFile(options.compareBaseline, 'utf-8'); + const baselineData = JSON.parse(baselineContent); + const baselineResults: ScenarioResult[] = Array.isArray(baselineData) + ? baselineData + : (baselineData.results ?? []); + + if (!options.json) { + // eslint-disable-next-line no-console + console.log(formatComparisonTable(results, baselineResults)); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to load baseline from ${options.compareBaseline}:`, error); + } + } + + return { results, exitCode: 0 }; +} diff --git a/scripts/benchmarks/i18n/init-env.mts b/scripts/benchmarks/i18n/init-env.mts new file mode 100644 index 000000000000..a1690cf576c8 --- /dev/null +++ b/scripts/benchmarks/i18n/init-env.mts @@ -0,0 +1,26 @@ +/** + * @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 Module from 'node:module'; +import path from 'node:path'; + +// Resolve dependencies from packages/angular/build/node_modules for runtime resolution in dist/ +const buildNodeModules = path.resolve( + import.meta.dirname, + '../../../packages/angular/build/node_modules', +); + +const currentPath = process.env.NODE_PATH ?? ''; +if (!currentPath.includes(buildNodeModules)) { + process.env.NODE_PATH = currentPath + ? `${buildNodeModules}${path.delimiter}${currentPath}` + : buildNodeModules; + // Initialize internal search paths for Node CommonJS loader + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Module as any)._initPaths?.(); +} diff --git a/scripts/benchmarks/i18n/reporters.mts b/scripts/benchmarks/i18n/reporters.mts new file mode 100644 index 000000000000..d5153552a2b3 --- /dev/null +++ b/scripts/benchmarks/i18n/reporters.mts @@ -0,0 +1,159 @@ +/** + * @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 os from 'node:os'; +import type { ScenarioResult } from './harness.mts'; + +export interface BenchmarkReportData { + timestamp: string; + system: { + nodeVersion: string; + platform: string; + arch: string; + cpus: number; + cpuModel: string; + totalMemoryMb: number; + }; + results: ScenarioResult[]; +} + +export function buildReportData(results: ScenarioResult[]): BenchmarkReportData { + const cpus = os.cpus(); + + return { + timestamp: new Date().toISOString(), + system: { + nodeVersion: process.version, + platform: os.platform(), + arch: os.arch(), + cpus: cpus.length, + cpuModel: cpus[0]?.model ?? 'unknown', + totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)), + }, + results, + }; +} + +export function formatJsonReport(results: ScenarioResult[]): string { + return JSON.stringify(buildReportData(results), null, 2); +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) { + return `${bytes} B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } + + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} + +function padRight(str: string, len: number): string { + return str.length >= len ? str : str + ' '.repeat(len - str.length); +} + +function padLeft(str: string, len: number): string { + return str.length >= len ? str : ' '.repeat(len - str.length) + str; +} + +export function formatConsoleTable(results: ScenarioResult[]): string { + const cpus = os.cpus(); + const gcStatus = typeof global.gc === 'function' ? 'active' : 'inactive (run with --expose-gc)'; + const header = + `================================================================================================================\n` + + `i18n Inliner Performance Benchmarks (Node ${process.version}, ${cpus.length} CPUs: ${cpus[0]?.model ?? ''} | GC: ${gcStatus})\n` + + `================================================================================================================\n`; + + const columns = [ + { name: 'Scenario', width: 25 }, + { name: 'Input Size', width: 11 }, + { name: 'Locales', width: 8 }, + { name: 'Mean Latency', width: 13 }, + { name: 'p50 / p95', width: 18 }, + { name: 'Throughput', width: 12 }, + { name: 'Peak Heap', width: 11 }, + { name: 'Peak RSS', width: 11 }, + ]; + + const colHeader = columns.map((col) => padRight(col.name, col.width)).join(' '); + const separator = columns.map((col) => '-'.repeat(col.width)).join(' '); + + const rows = results.map((r) => { + const inputFormatted = formatBytes(r.inputSizeBytes); + const meanFormatted = `${r.meanMs.toFixed(1)} ms`; + const p50p95Formatted = `${r.medianMs.toFixed(0)} ms / ${r.p95Ms.toFixed(0)} ms`; + const throughputFormatted = `${r.throughputMBps.toFixed(1)} MB/s`; + const peakHeapFormatted = formatBytes(r.peakHeapBytes ?? 0); + const peakRssFormatted = formatBytes(r.peakRssBytes); + + return [ + padRight(r.name, columns[0].width), + padLeft(inputFormatted, columns[1].width), + padLeft(r.localeCount.toString(), columns[2].width), + padLeft(meanFormatted, columns[3].width), + padLeft(p50p95Formatted, columns[4].width), + padLeft(throughputFormatted, columns[5].width), + padLeft(peakHeapFormatted, columns[6].width), + padLeft(peakRssFormatted, columns[7].width), + ].join(' '); + }); + + return `${header}\n${colHeader}\n${separator}\n${rows.join('\n')}\n${'='.repeat(separator.length)}\n`; +} + +export function formatComparisonTable( + currentResults: ScenarioResult[], + baselineResults: ScenarioResult[], +): string { + const header = + `====================================================================================================\n` + + `i18n Inliner Benchmark Comparison (Current vs Baseline)\n` + + `====================================================================================================\n`; + + const columns = [ + { name: 'Scenario', width: 26 }, + { name: 'Baseline Mean', width: 14 }, + { name: 'Current Mean', width: 14 }, + { name: 'Latency Diff', width: 14 }, + { name: 'Baseline MB/s', width: 14 }, + { name: 'Current MB/s', width: 14 }, + ]; + + const colHeader = columns.map((col) => padRight(col.name, col.width)).join(' '); + const separator = columns.map((col) => '-'.repeat(col.width)).join(' '); + + const rows = currentResults.map((curr) => { + const base = baselineResults.find((b) => b.name === curr.name); + if (!base) { + return [ + padRight(curr.name, columns[0].width), + padLeft('N/A', columns[1].width), + padLeft(`${curr.meanMs.toFixed(1)} ms`, columns[2].width), + padLeft('NEW', columns[3].width), + padLeft('N/A', columns[4].width), + padLeft(`${curr.throughputMBps.toFixed(1)} MB/s`, columns[5].width), + ].join(' '); + } + + const diffPercent = ((curr.meanMs - base.meanMs) / base.meanMs) * 100; + const diffSign = diffPercent > 0 ? '+' : ''; + const diffText = `${diffSign}${diffPercent.toFixed(1)}% ${diffPercent > 1 ? '(slower)' : diffPercent < -1 ? '(faster)' : '(same)'}`; + + return [ + padRight(curr.name, columns[0].width), + padLeft(`${base.meanMs.toFixed(1)} ms`, columns[1].width), + padLeft(`${curr.meanMs.toFixed(1)} ms`, columns[2].width), + padLeft(diffText, columns[3].width), + padLeft(`${base.throughputMBps.toFixed(1)} MB/s`, columns[4].width), + padLeft(`${curr.throughputMBps.toFixed(1)} MB/s`, columns[5].width), + ].join(' '); + }); + + return `${header}\n${colHeader}\n${separator}\n${rows.join('\n')}\n${'='.repeat(separator.length)}\n`; +} diff --git a/scripts/benchmarks/i18n/scenarios.mts b/scripts/benchmarks/i18n/scenarios.mts new file mode 100644 index 000000000000..c00f34ded8a2 --- /dev/null +++ b/scripts/benchmarks/i18n/scenarios.mts @@ -0,0 +1,476 @@ +/** + * @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 './init-env.mts'; + +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import type { BuildOutputFile } from '../../../packages/angular/build/src/tools/esbuild/bundler-files.js'; +import type { LocaleInlineOptions } from '../../../packages/angular/build/src/tools/esbuild/i18n-inliner.js'; +import { generateSyntheticBundle, generateTranslations, initializeFixtures } from './fixtures.mts'; +import type { BenchmarkScenario } from './harness.mts'; + +const requireFromBuild = createRequire( + path.resolve(import.meta.dirname, '../../../packages/angular/build/package.json'), +); + +const { I18nInliner } = requireFromBuild( + '../../../dist/@angular/build/src/tools/esbuild/i18n-inliner.js', +) as typeof import('../../../packages/angular/build/src/tools/esbuild/i18n-inliner.js'); + +export interface ScenarioFactoryOptions { + concurrency?: number; +} + +const DEFAULT_LOCALES_8 = ['fr', 'de', 'es', 'ja', 'zh', 'it', 'pt', 'ko']; +const DEFAULT_LOCALES_32 = [ + 'fr', + 'de', + 'es', + 'ja', + 'zh', + 'it', + 'pt', + 'ko', + 'ru', + 'pl', + 'nl', + 'tr', + 'ar', + 'hi', + 'sv', + 'da', + 'fi', + 'no', + 'cs', + 'el', + 'he', + 'hu', + 'id', + 'ms', + 'ro', + 'sk', + 'th', + 'uk', + 'vi', + 'bg', + 'hr', + 'sr', +]; + +interface GeneratedWorkload { + files: BuildOutputFile[]; + locales: LocaleInlineOptions[]; + totalInputSizeBytes: number; +} + +function createBundleSet( + mainSizeBytes: number, + chunkCount: number, + chunkSizeBytes: number, + messageCount: number, + withSourceMap: boolean, +): BuildOutputFile[] { + const files: BuildOutputFile[] = []; + + const mainMessages = Math.floor(messageCount * 0.4); + const { codeFile: mainCode, mapFile: mainMap } = generateSyntheticBundle({ + filename: 'main.js', + targetByteSize: mainSizeBytes, + messageCount: mainMessages, + withSourceMap, + messageIdOffset: 0, + }); + + files.push(mainCode); + if (mainMap) { + files.push(mainMap); + } + + const remainingMessages = messageCount - mainMessages; + const messagesPerChunk = Math.max(1, Math.floor(remainingMessages / chunkCount)); + + for (let i = 0; i < chunkCount; i++) { + const { codeFile, mapFile } = generateSyntheticBundle({ + filename: `chunk_${i}.js`, + targetByteSize: chunkSizeBytes, + messageCount: messagesPerChunk, + withSourceMap, + messageIdOffset: mainMessages + i * messagesPerChunk, + }); + + files.push(codeFile); + if (mapFile) { + files.push(mapFile); + } + } + + return files; +} + +function calculateInputSizeBytes(files: BuildOutputFile[]): number { + return files.reduce((total, f) => { + // Only count JS code size towards raw input volume (maps are auxiliary) + return f.path.endsWith('.js') ? total + f.size : total; + }, 0); +} + +/** + * Note on Worker Pool Lifecycle: + * Each scenario's run() method instantiates and closes an I18nInliner per iteration. + * This is designed as a macro benchmark to reflect the cold-start behavior of single-shot + * CLI build invocations (including worker thread pool initialization, task dispatch, + * inlining transformations, and thread pool shutdown). Warmup iterations warm up the + * main-thread V8 isolate and runtime paths, while worker threads are initialized per iteration. + */ + +/** + * 1. Standard App Scenario: + * 1 main bundle (1 MB) + 20 route chunks (50 KB) = ~2 MB input JS, 8 locales, maps enabled. + */ +export function createStandardAppScenario(options: ScenarioFactoryOptions = {}): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'standard-app', + description: 'Standard App: 1 main bundle (1 MB) + 20 chunks (50 KB), 8 locales, sourcemaps ON', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_8.length; + }, + async setup() { + await initializeFixtures(); + const files = createBundleSet(1024 * 1024, 20, 50 * 1024, 1000, true); + const locales = generateTranslations(DEFAULT_LOCALES_8, 1000); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + +/** + * 2. Standard App without Sourcemaps: + * Evaluates inlining without source map generation and remapping. + */ +export function createStandardAppNoMapsScenario( + options: ScenarioFactoryOptions = {}, +): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'standard-app-no-maps', + description: + 'Standard App (No Maps): 1 main bundle (1 MB) + 20 chunks (50 KB), 8 locales, sourcemaps OFF', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_8.length; + }, + async setup() { + await initializeFixtures(); + const files = createBundleSet(1024 * 1024, 20, 50 * 1024, 1000, false); + const locales = generateTranslations(DEFAULT_LOCALES_8, 1000); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + +/** + * 3. Enterprise Multilingual Scenario: + * 1 main bundle (2 MB) + 40 chunks (60 KB), 32 locales, 3,000 translations, sourcemaps ON. + * Exercises sliding window batching (4 windows of 8 locales) and memory scaling. + */ +export function createEnterpriseScenario(options: ScenarioFactoryOptions = {}): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'enterprise-multilingual', + description: 'Enterprise: 1 main (2 MB) + 40 chunks (60 KB), 32 locales, sourcemaps ON', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_32.length; + }, + async setup() { + await initializeFixtures(); + const files = createBundleSet(2 * 1024 * 1024, 40, 60 * 1024, 3000, true); + const locales = generateTranslations(DEFAULT_LOCALES_32, 3000); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + +/** + * 4. Monolithic Dominant Scenario: + * 1 dominant bundle (6 MB) + 5 small runtime chunks (30 KB), 8 locales, sourcemaps ON. + * Evaluates whether LPT + DOMINANT_FILE_RATIO sharding saturates workers efficiently. + */ +export function createMonolithicScenario(options: ScenarioFactoryOptions = {}): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'monolithic-dominant', + description: 'Monolithic: 1 dominant bundle (6 MB) + 5 small chunks (30 KB), 8 locales', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_8.length; + }, + async setup() { + await initializeFixtures(); + const files = createBundleSet(6 * 1024 * 1024, 5, 30 * 1024, 2000, true); + const locales = generateTranslations(DEFAULT_LOCALES_8, 2000); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + +/** + * Helper to prime the persistent cache in an isolated process. + */ +export async function primeCache(cacheDir: string, concurrency?: number): Promise { + await initializeFixtures(); + const files = createBundleSet(1024 * 1024, 15, 50 * 1024, 1000, true); + const locales = generateTranslations(DEFAULT_LOCALES_8, 1000); + const primer = new I18nInliner({ + missingTranslation: 'warning', + persistentCachePath: cacheDir, + maxConcurrency: concurrency, + }); + await primer.inlineAll(files, locales); + await primer.close(); +} + +/** + * 5. Persistent Cache Warm Scenario: + * Evaluates throughput when 100% of transformed files and translations are pre-cached in LMDB. + */ +export function createPersistentCacheWarmScenario( + options: ScenarioFactoryOptions = {}, +): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + let cacheDir: string | undefined; + + return { + name: 'persistent-cache-warm', + description: + 'Persistent Cache (Warm): 100% LMDB cache hits for transformed files & translations', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_8.length; + }, + async setup() { + await initializeFixtures(); + cacheDir = await fs.mkdtemp(path.join(os.tmpdir(), 'angular-i18n-bench-cache-')); + const files = createBundleSet(1024 * 1024, 15, 50 * 1024, 1000, true); + const locales = generateTranslations(DEFAULT_LOCALES_8, 1000); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + + // Prime the persistent cache out-of-process so cold worker thread allocations + // do not inflate this process's RSS metrics. + const scenariosUrl = pathToFileURL(path.resolve(import.meta.dirname, './scenarios.mts')).href; + const primerCode = + `import { primeCache } from ${JSON.stringify(scenariosUrl)};\n` + + `await primeCache(${JSON.stringify(cacheDir)}, ${options.concurrency ?? 'undefined'});\n`; + + const primerProc = spawnSync( + process.execPath, + [ + '--no-warnings=ExperimentalWarning', + '--experimental-transform-types', + '--input-type=module', + '-e', + primerCode, + ], + { stdio: 'inherit' }, + ); + + if (primerProc.status !== 0 || primerProc.error) { + throw new Error( + `Failed to prime cache for persistent-cache-warm scenario: ${primerProc.error?.message ?? primerProc.status}`, + ); + } + }, + + async run() { + if (!workload || !cacheDir) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + persistentCachePath: cacheDir, + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + async teardown() { + if (cacheDir) { + await fs.rm(cacheDir, { recursive: true, force: true }).catch(() => {}); + } + }, + }; +} + +/** + * 6. Large Enterprise (10k translations) Scenario: + * 1 main bundle (3 MB) + 100 chunks (50 KB), 32 locales, 10,000 translations, sourcemaps ON. + * Maximum scale stress test for binary translation tables, memory retention, and multi-locale windows. + */ +export function createLargeEnterpriseScenario( + options: ScenarioFactoryOptions = {}, +): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'large-enterprise-10k', + description: + 'Large Enterprise (10k msgs): 1 main (3 MB) + 100 chunks (50 KB), 32 locales, 10,000 translations', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_32.length; + }, + async setup() { + await initializeFixtures(); + const files = createBundleSet(3 * 1024 * 1024, 100, 50 * 1024, 10000, true); + const locales = generateTranslations(DEFAULT_LOCALES_32, 10000); + + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + +export function getAllScenarios(options: ScenarioFactoryOptions = {}): BenchmarkScenario[] { + return [ + createStandardAppScenario(options), + createStandardAppNoMapsScenario(options), + createEnterpriseScenario(options), + createLargeEnterpriseScenario(options), + createMonolithicScenario(options), + createPersistentCacheWarmScenario(options), + ]; +} + +export function getScenarioByName( + name: string, + options: ScenarioFactoryOptions = {}, +): BenchmarkScenario | undefined { + const all = getAllScenarios(options); + + return all.find((s) => s.name.toLowerCase() === name.toLowerCase()); +} diff --git a/tests/e2e/tests/commands/config/config-global-validation.ts b/tests/e2e/tests/commands/config/config-global-validation.ts index 7be29130dca0..39fa611075a9 100644 --- a/tests/e2e/tests/commands/config/config-global-validation.ts +++ b/tests/e2e/tests/commands/config/config-global-validation.ts @@ -9,21 +9,15 @@ export default async function () { let ngError: Error; ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted', 'true')); - assert.match( - ngError.message, - /Data path "\/cli" must NOT have additional properties\(completion\)\./, - ); + assert.match(ngError.message, /Unknown option "completion" at "\/cli"\./); ngError = await expectToFail(() => silentNg('config', '--global', 'cli.completion.invalid', 'true'), ); - assert.match( - ngError.message, - /Data path "\/cli\/completion" must NOT have additional properties\(invalid\)\./, - ); + assert.match(ngError.message, /Unknown option "invalid" at "\/cli\/completion"\./); ngError = await expectToFail(() => silentNg('config', '--global', 'cli.cache.enabled', 'true')); - assert.match(ngError.message, /Data path "\/cli" must NOT have additional properties\(cache\)\./); + assert.match(ngError.message, /Unknown option "cache" at "\/cli"\./); ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted')); assert.match(ngError.message, /Value cannot be found\./); diff --git a/tests/e2e/tests/commands/config/config-set.ts b/tests/e2e/tests/commands/config/config-set.ts index 2152e573132e..05fa364bcb37 100644 --- a/tests/e2e/tests/commands/config/config-set.ts +++ b/tests/e2e/tests/commands/config/config-set.ts @@ -6,10 +6,7 @@ export default async function () { let ngError: Error; ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz', 'true')); - assert.match( - ngError.message, - /Data path "\/cli\/warnings" must NOT have additional properties\(zzzz\)\./, - ); + assert.match(ngError.message, /Unknown option "zzzz" at "\/cli\/warnings"\./); ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz')); assert.match(ngError.message, /Value cannot be found\./);