diff --git a/.bazelignore b/.bazelignore index 52ebdac6af4a..9ca3992b8666 100644 --- a/.bazelignore +++ b/.bazelignore @@ -17,4 +17,3 @@ packages/ngtools/webpack/node_modules packages/schematics/angular/node_modules modules/testing/builder/node_modules tests/node_modules -tools/baseline_browserslist/node_modules diff --git a/.bazelrc b/.bazelrc index 4f79c86cf3b4..816134dca1ef 100644 --- a/.bazelrc +++ b/.bazelrc @@ -16,6 +16,9 @@ test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test # The below is useful to while using `fit` and `fdescribe` to avoid sharing and re-runs of failed flaky tests. test:no-sharding --flaky_test_attempts=1 --test_sharding_strategy=disabled +# Frozen lockfile +common --lockfile_mode=error + ############################### # Filesystem interactions # ############################### diff --git a/.bazelversion b/.bazelversion index e8be68404bcb..1985849fb589 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.6.1 +7.7.0 diff --git a/.github/shared-actions/windows-bazel-test/action.yml b/.github/shared-actions/windows-bazel-test/action.yml index cb72d4febd61..5e2ec1777892 100644 --- a/.github/shared-actions/windows-bazel-test/action.yml +++ b/.github/shared-actions/windows-bazel-test/action.yml @@ -1,78 +1,42 @@ -name: 'Native Windows Bazel e2e test' -description: 'Runs an Angular CLI e2e Bazel test on native Windows (dispatched from inside WSL)' -author: 'Angular' +name: Native Windows Bazel E2E test +description: Runs an Angular CLI e2e Bazel test on native Windows +author: Angular inputs: test_target_name: - description: E2E test target name + description: E2E test target name. required: true - test_args: - description: | - Text representing the command line arguments that - should be passed to the e2e test runner. + e2e_temp_dir: + description: 'The temporary directory path for E2E tests.' required: false - default: '' + # Use D:\\ by default as it's much faster + # See: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows + default: 'D:\\tmp_dir' runs: - using: composite + using: 'composite' steps: - - name: Initialize WSL - id: init_wsl - uses: angular/dev-infra/github-actions/setup-wsl@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 - with: - wsl_firewall_interface: 'vEthernet (WSL (Hyper-V firewall))' - - - name: Installing pnpm (in WSL) - run: npm install -g pnpm@9 - shell: wsl-bash {0} - - - name: Install node modules in WSL (re-using from previous install/cache restore) - run: | - cd ${{steps.init_wsl.outputs.repo_path}} - pnpm install --frozen-lockfile - shell: wsl-bash {0} - - - name: Build test binary for Windows (inside WSL) - shell: wsl-bash {0} - run: | - cd ${{steps.init_wsl.outputs.repo_path}} - pnpm bazel \ - build --config=e2e //tests/legacy-cli:${{inputs.test_target_name}} --platforms=tools:windows_x64 - env: - # See: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows - WSLENV: 'GOOGLE_APPLICATION_CREDENTIALS/p' - - - name: Copying binary artifact to host - shell: wsl-bash {0} + - name: Set up temp directory + shell: bash run: | - cd ${{steps.init_wsl.outputs.repo_path}} - tar -cf /tmp/test.tar.gz dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_ - mkdir /mnt/c/test - mv /tmp/test.tar.gz /mnt/c/test - (cd /mnt/c/test && tar -xf /mnt/c/test/test.tar.gz) + mkdir ${{ inputs.e2e_temp_dir }} - name: Convert symlinks for Windows host - shell: wsl-bash {0} + shell: pwsh run: | - cd ${{steps.init_wsl.outputs.repo_path}} - - runfiles_dir="/mnt/c/test/dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles" - - # Make WSL symlinks compatible on Windows native file system. - node scripts/windows-testing/convert-symlinks.mjs $runfiles_dir "${{steps.init_wsl.outputs.cmd_path}}" + $runfiles_dir = "./dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles" # Needed for resolution because Aspect/Bazel looks for repositories at `/external`. # TODO(devversion): consult with Aspect on why this is needed. - (cd $runfiles_dir/_main && ${{steps.init_wsl.outputs.cmd_path}} /C "mklink /D external ..") + Set-Location -Path "${runfiles_dir}\_main" + New-Item -ItemType SymbolicLink -Path "external" -Target ".." - - name: Run tests - # Note: This is Git Bash. + - name: Run CLI E2E tests shell: bash env: BAZEL_BINDIR: '.' - working-directory: "C:\\test" + E2E_TEMP: ${{ inputs.e2e_temp_dir }} run: | - node "${{github.workspace}}\\scripts\\windows-testing\\parallel-executor.mjs" \ - $PWD/dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles \ - ${{inputs.test_target_name}} \ - "${{inputs.test_args}}" \ + node ./scripts/windows-testing/parallel-executor.mjs \ + "./dist/bin/tests/legacy-cli/${{ inputs.test_target_name }}_/${{ inputs.test_target_name }}.bat.runfiles" \ + ${{ inputs.test_target_name }} diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml index f55b9c8510c9..eee4e99dd25b 100644 --- a/.github/workflows/assistant-to-the-branch-manager.yml +++ b/.github/workflows/assistant-to-the-branch-manager.yml @@ -16,6 +16,6 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: angular/dev-infra/github-actions/branch-manager@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + - uses: angular/dev-infra/github-actions/branch-manager@75d22eaeab72c5f03da29038c3f367a36c42017a with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 882250aa1c95..f792d983cdd9 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@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - 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@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -58,22 +58,20 @@ jobs: test: needs: build - runs-on: ubuntu-latest-4core + runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules run: pnpm install --frozen-lockfile - name: Run module and package tests - run: pnpm bazel test //modules/... //packages/... - env: - ASPECT_RULES_JS_FROZEN_PNPM_LOCK: '1' + run: pnpm bazel test -- //... -//tests/legacy-cli/... e2e: needs: test @@ -87,19 +85,50 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - 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/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} - e2e_windows: + build-e2e-windows: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Build E2E tests for Windows on Linux + run: | + pnpm bazel build \ + --config=e2e \ + //tests/legacy-cli:e2e.npm_node22 \ + //tests/legacy-cli:e2e.esbuild_node22 \ + --platforms=tools:windows_x64 + - name: Store built Windows E2E tests + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: win-e2e-build-artifacts + path: | + dist/bin/tests/legacy-cli/** + !**/node_modules/** + retention-days: 1 + if-no-files-found: 'error' + + e2e-windows: + needs: build-e2e-windows strategy: fail-fast: false matrix: @@ -110,14 +139,14 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 - - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 - - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Download built Windows E2E tests + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: - allow_windows_rbe: true - google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + name: win-e2e-build-artifacts + path: dist/bin/tests/legacy-cli/ - name: Run CLI E2E tests uses: ./.github/shared-actions/windows-bazel-test with: @@ -138,13 +167,13 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -163,13 +192,13 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -183,13 +212,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@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run E2E Browser tests @@ -206,7 +235,7 @@ jobs: ./scripts/saucelabs/wait-for-tunnel.sh pnpm bazel test --config=saucelabs //tests/legacy-cli:e2e.saucelabs ./scripts/saucelabs/stop-tunnel.sh - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 if: ${{ failure() }} with: name: sauce-connect-log @@ -219,11 +248,11 @@ jobs: CIRCLE_BRANCH: ${{ github.ref_name }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - run: pnpm admin snapshots --verbose env: SNAPSHOT_BUILDS_GITHUB_TOKEN: ${{ secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f2bb3940a3b6..fb4789033a1a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,12 +23,12 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: languages: javascript-typescript build-mode: none config-file: .github/codeql/config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: category: '/language:javascript-typescript' diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml index 7d8f337c4225..238eb0ea5d88 100644 --- a/.github/workflows/dev-infra.yml +++ b/.github/workflows/dev-infra.yml @@ -13,13 +13,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: angular/dev-infra/github-actions/pull-request-labeling@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + - uses: angular/dev-infra/github-actions/pull-request-labeling@75d22eaeab72c5f03da29038c3f367a36c42017a with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} post_approval_changes: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: angular/dev-infra/github-actions/post-approval-changes@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + - uses: angular/dev-infra/github-actions/post-approval-changes@75d22eaeab72c5f03da29038c3f367a36c42017a with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/feature-requests.yml b/.github/workflows/feature-requests.yml index 032707e874f5..bb703875e0d7 100644 --- a/.github/workflows/feature-requests.yml +++ b/.github/workflows/feature-requests.yml @@ -16,6 +16,6 @@ jobs: if: github.repository == 'angular/angular-cli' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/feature-request@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + - uses: angular/dev-infra/github-actions/feature-request@75d22eaeab72c5f03da29038c3f367a36c42017a with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index ea9aa9331c1d..c062d0ebfc58 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -23,7 +23,7 @@ jobs: workflows: ${{ steps.workflows.outputs.workflows }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - id: workflows @@ -38,16 +38,16 @@ jobs: workflow: ${{ fromJSON(needs.list.outputs.workflows) }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - 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 # identity federation. This allows us to request short lived credentials on demand, rather than storing # credentials in secrets long term. More information can be found at: # https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-google-cloud-platform - - uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5 # v2.1.12 + - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: project_id: 'internal-200822' workload_identity_provider: 'projects/823469418460/locations/global/workloadIdentityPools/measurables-tracking/providers/angular' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2adae6939dbb..93bb992d9cc1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,11 +34,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup ESLint Caching - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: .eslintcache key: ${{ runner.os }}-${{ hashFiles('.eslintrc.json') }} @@ -56,7 +56,7 @@ jobs: - name: Run Validation run: pnpm admin validate - name: Check Package Licenses - uses: angular/dev-infra/github-actions/linting/licenses@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/linting/licenses@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Check tooling setup run: pnpm check-tooling-setup - name: Check commit message @@ -72,17 +72,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Build release targets run: pnpm ng-dev release build - name: Store PR release packages - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: packages path: dist/releases/*.tgz @@ -90,20 +90,18 @@ jobs: test: needs: build - runs-on: ubuntu-latest-16core + runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Run module and package tests - run: pnpm bazel test //modules/... //packages/... - env: - ASPECT_RULES_JS_FROZEN_PNPM_LOCK: '1' + run: pnpm bazel test -- //... -//tests/legacy-cli/... e2e: needs: build @@ -117,34 +115,63 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a - 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/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} - # Temporarily disabled due to https://github.com/Vampire/setup-wsl/issues/76. - # e2e-windows-subset: - # needs: build - # runs-on: windows-2025 - # steps: - # - name: Initialize environment - # uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@59c46175bf3a8870c0c2ceb9de1eb741fd50d415 - # - name: Setup Bazel - # uses: angular/dev-infra/github-actions/bazel/setup@59c46175bf3a8870c0c2ceb9de1eb741fd50d415 - # - name: Setup Bazel RBE - # uses: angular/dev-infra/github-actions/bazel/configure-remote@59c46175bf3a8870c0c2ceb9de1eb741fd50d415 - # with: - # allow_windows_rbe: true - # - name: Run CLI E2E tests - # uses: ./.github/shared-actions/windows-bazel-test - # with: - # test_target_name: e2e_node22 - # test_args: --esbuild --glob "tests/basic/{build,rebuild}.ts" + build-e2e-windows-subset: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Build E2E tests for Windows on Linux + run: | + pnpm bazel build \ + --config=e2e \ + //tests/legacy-cli:e2e.esbuild_node22 \ + --platforms=tools:windows_x64 + - name: Store built Windows E2E tests + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: win-e2e-build-artifacts + path: | + dist/bin/tests/legacy-cli/** + !**/node_modules/** + retention-days: 1 + if-no-files-found: 'error' + + e2e-windows-subset: + needs: build-e2e-windows-subset + runs-on: windows-2025 + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Download built Windows E2E tests + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: win-e2e-build-artifacts + path: dist/bin/tests/legacy-cli/ + - name: Run CLI E2E tests + uses: ./.github/shared-actions/windows-bazel-test + with: + test_target_name: e2e.esbuild_node22 + env: + E2E_SHARD_TOTAL: 1 + TESTBRIDGE_TEST_ONLY: tests/basic/{build,rebuild}.ts e2e-package-managers: needs: build @@ -158,13 +185,13 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a - 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/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} @@ -181,12 +208,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/setup@75d22eaeab72c5f03da29038c3f367a36c42017a - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@fbdd8b7df383ae8fb34907a98353c1e8f0f5e528 + uses: angular/dev-infra/github-actions/bazel/configure-remote@75d22eaeab72c5f03da29038c3f367a36c42017a - 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/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b2ed77eab147..5b82ca3bd6e6 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: 'Run analysis' - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif @@ -38,7 +38,7 @@ jobs: # Upload the results as artifacts. - name: 'Upload artifact' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: SARIF file path: results.sarif @@ -46,6 +46,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index de3ad9a9154d..83582f8ece43 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ jsconfig.json # VSCode # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore -.vscode/ +.vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json diff --git a/.ng-dev/commit-message.mjs b/.ng-dev/commit-message.mjs index 94fe91d8f727..4a3b270ce0a7 100644 --- a/.ng-dev/commit-message.mjs +++ b/.ng-dev/commit-message.mjs @@ -9,7 +9,6 @@ export const commitMessage = { maxLineLength: Infinity, minBodyLength: 0, minBodyLengthTypeExcludes: ['docs'], - disallowFixup: true, // Note: When changing this logic, also change the `contributing.ejs` file. scopes: packages.map(({ name }) => name), }; diff --git a/.ng-dev/pull-request.mjs b/.ng-dev/pull-request.mjs index 8beefa10c5fd..1f2d84d7ccba 100644 --- a/.ng-dev/pull-request.mjs +++ b/.ng-dev/pull-request.mjs @@ -1,12 +1,12 @@ /** * Configuration for the merge tool in `ng-dev`. This sets up the labels which * are respected by the merge script (e.g. the target labels). - * + * * @type { import("@angular/ng-dev").PullRequestConfig } */ export const pullRequest = { githubApiMerge: { - default: 'rebase', + default: 'auto', labels: [{ pattern: 'merge: squash commits', method: 'squash' }], }, }; diff --git a/.nvmrc b/.nvmrc index 91d5f6ff8e3f..5767036af0e2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.18.0 +22.21.1 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000000..6f3e0e3d3375 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,49 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.2.0", + "configurations": [ + { + "name": "DEBUG: Attach to bazel test", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "timeout": 600000, + "sourceMaps": true, + "skipFiles": ["/**"], + "sourceMapPathOverrides": { + "?:*@0.0.0/node_modules/@angular-devkit/build-angular/*": "${workspaceFolder}/packages/angular_devkit/build_angular/*", + "?:*@0.0.0/node_modules/@angular-devkit/build-webpack/*": "${workspaceFolder}/packages/angular_devkit/build_webpack/*", + "?:*@0.0.0/node_modules/@angular-devkit/*": "${workspaceFolder}/packages/angular_devkit/*", + "?:*@0.0.0/node_modules/@angular/*": "${workspaceFolder}/packages/angular/*", + "?:*/bin/*": "${workspaceFolder}/*" + }, + "resolveSourceMapLocations": ["*/**", "!**/rxjs**"] + }, + { + "name": "DEBUG: Run bazel test (Custom Target)", + "type": "node", + "request": "launch", + "restart": true, + "timeout": 600000, + "runtimeExecutable": "pnpm", + "runtimeArgs": ["bazel", "test", "${input:bazelTarget}", "--config=debug"], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}" + } + ], + "compounds": [ + { + "name": "DEBUG: Run bazel test (Custom Target) + Attach", + "configurations": ["DEBUG: Attach to bazel test", "DEBUG: Run bazel test (Custom Target)"] + } + ], + "inputs": [ + { + "id": "bazelTarget", + "type": "promptString", + "description": "Enter the Bazel test target (e.g., //path/to/my:unit_test)", + "default": "//packages/..." + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..b98a874af297 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "[javascript]": { + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.formatOnSave": true + }, + // Exclude third party modules and build artifacts from the editor watchers/searches. + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "**/bazel-out/**": true, + "**/dist/**": true, + "**/dist-schema/**": true + }, + "search.exclude": { + "**/node_modules": true, + "**/bazel-out": true, + "**/dist": true, + "**/dist-schema": true, + ".history": true + }, + "git.ignoreLimitWarning": true, + "gitlens.advanced.blame.customArguments": ["--ignore-revs-file .git-blame-ignore-revs"] +} diff --git a/BUILD.bazel b/BUILD.bazel index 9518c3a6f0bb..57090fe9772a 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,5 +1,6 @@ load("@aspect_rules_ts//ts:defs.bzl", rules_js_tsconfig = "ts_config") load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") +load("@devinfra//bazel/validation:defs.bzl", "validate_ts_version_matching") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "copy_to_bin") @@ -18,11 +19,19 @@ exports_files([ npm_link_all_packages() +rules_js_tsconfig( + name = "tsconfig", + src = "tsconfig.json", + visibility = [ + "//:__pkg__", + ], +) + rules_js_tsconfig( name = "build-tsconfig", src = "tsconfig-build.json", deps = [ - "tsconfig.json", + ":tsconfig", "//:node_modules/@types/node", ], ) @@ -31,7 +40,7 @@ rules_js_tsconfig( name = "test-tsconfig", src = "tsconfig-test.json", deps = [ - "tsconfig.json", + ":tsconfig", "//:node_modules/@types/jasmine", "//:node_modules/@types/node", ], @@ -41,7 +50,7 @@ rules_js_tsconfig( name = "build-tsconfig-esm", src = "tsconfig-build-esm.json", deps = [ - "tsconfig.json", + ":tsconfig", ], ) @@ -94,3 +103,20 @@ config_setting( ":enable_snapshot_repo_deps": "true", }, ) + +validate_ts_version_matching( + module_lock_file = "MODULE.bazel.lock", + package_json = "package.json", +) + +# This is needed following https://github.com/bazel-contrib/rules_nodejs/pull/3859 +toolchain( + name = "node22_windows_no_exec_config_toolchain", + exec_compatible_with = [], + target_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + toolchain = "@node22_windows_amd64//:toolchain", + toolchain_type = "@rules_nodejs//nodejs:toolchain_type", +) diff --git a/CHANGELOG.md b/CHANGELOG.md index 605cddd12017..22b275a5128b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,103 +1,857 @@ - + + +# 21.0.0-rc.1 (2025-11-05) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [dfb4242b3](https://github.com/angular/angular-cli/commit/dfb4242b347365f3a2c6d006f07a16c982ff4dbe) | fix | add vitest to version command output | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [f89750b27](https://github.com/angular/angular-cli/commit/f89750b27866c307da546fe4f33da980693ca5c1) | fix | add `addImports` option to jasmine-vitest schematic | +| [515b09c4f](https://github.com/angular/angular-cli/commit/515b09c4f28ef1c2eb911cb73135a6086dcab3c9) | fix | add Vitest config generation and runner checks | +| [0e83fe1a8](https://github.com/angular/angular-cli/commit/0e83fe1a87cc3dcbc9daa4440a050ae6aafc8042) | fix | add warnings and improve Karma config generation | +| [b91fa31f2](https://github.com/angular/angular-cli/commit/b91fa31f20b49ead021c72c271f67da38b340584) | fix | align Karma project generation with unified unit-test builder | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [62938e799](https://github.com/angular/angular-cli/commit/62938e79977d14045b7883d459d786dbb8d4d7ee) | fix | update vitest to 4.0.6 and remove coverage workaround | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [5d76d84e6](https://github.com/angular/angular-cli/commit/5d76d84e64db717e177b77f7a07ee8819b258877) | fix | improve locale handling in app-engine | +| [4a3cfdfce](https://github.com/angular/angular-cli/commit/4a3cfdfce75b7cedf64e26d85b0a92ec92ddd12d) | fix | improve route matching for wildcard routes | + + + + + +# 20.3.9 (2025-11-05) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [08e07e338](https://github.com/angular/angular-cli/commit/08e07e338edd799400e18cd62cd131b6d408f4cf) | fix | improve locale handling in app-engine | +| [683697ebc](https://github.com/angular/angular-cli/commit/683697ebc5e129d2b17bded9e4ff318d598e0bd3) | fix | improve route matching for wildcard routes | + + + + + +# 21.0.0-rc.0 (2025-10-30) + + + + + +# 21.0.0-next.10 (2025-10-29) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [c967a447c](https://github.com/angular/angular-cli/commit/c967a447ce755fbf582ec35aa24bb6e0fa0043cf) | fix | correct spacing in application spec tsconfig | +| [00d941c43](https://github.com/angular/angular-cli/commit/00d941c433de718cf3c38033d5d68dd86f790291) | fix | correct style guide paths for standalone components | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [6e395fc0c](https://github.com/angular/angular-cli/commit/6e395fc0c4505dd32b3237ea116e8db6bde25758) | fix | ensure vitest code coverage handles virtual files correctly | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [b2f048773](https://github.com/angular/angular-cli/commit/b2f048773c6014022983e7ccc52cb760619d8a1b) | fix | add --ui option for Vitest runner | +| [530d9270e](https://github.com/angular/angular-cli/commit/530d9270e87786594dd8d1956b0806a28650db25) | fix | add `define` option to dev-server | +| [091d1c03e](https://github.com/angular/angular-cli/commit/091d1c03e9665371f52c4cb8ec6cd9f1f862a407) | fix | add adapters to new reporter | +| [542d52868](https://github.com/angular/angular-cli/commit/542d528683cc0e51fd5a55ed6dbf43168f7a51a5) | fix | allow custom runner configuration file for unit-test | +| [9975c7249](https://github.com/angular/angular-cli/commit/9975c72494bc2a71359dc4c5a31e43eed040716e) | fix | ensure locale data plugin runs before other plugins | +| [7c529c1bc](https://github.com/angular/angular-cli/commit/7c529c1bc606101ab8c506e0b7845d2e9f509db4) | fix | externalize Angular dependencies in Vitest runner | +| [a41f4e860](https://github.com/angular/angular-cli/commit/a41f4e860dd28bee28af47c1513f55450ca74b5f) | fix | handle redirects from guards during prerendering | +| [bab5806c2](https://github.com/angular/angular-cli/commit/bab5806c281fd4cdd63b7969e691d703ed1e7680) | fix | introduce vitest-base.config for test configuration | +| [9132e6af9](https://github.com/angular/angular-cli/commit/9132e6af9fd573d8b39c69a50b4b93e256145fd4) | fix | resolve browser provider packages using project resolver | + + + + + +# 20.3.8 (2025-10-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [813cba9b9](https://github.com/angular/angular-cli/commit/813cba9b9bfe60e874595ce25608ca85a890f6bf) | fix | expand jest and jest-environment-jsdom to allow version 30 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [542973ab0](https://github.com/angular/angular-cli/commit/542973ab074ccd9a5f09f73ee7f2706a21db45fc) | fix | add adapters to new reporter | +| [f0885691d](https://github.com/angular/angular-cli/commit/f0885691d18b6575351774fcc50d746d981285f6) | fix | ensure locale data plugin runs before other plugins | +| [45e498f95](https://github.com/angular/angular-cli/commit/45e498f9576ff83eebe02deb235d36498ce06bde) | fix | handle redirects from guards during prerendering | + + + + + +# 19.2.19 (2025-10-29) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [4d8ea27a1](https://github.com/angular/angular-cli/commit/4d8ea27a1726709b8398a26915395e7611571dae) | fix | update vite to v6.4.1 | + + + + + +# 21.0.0-next.9 (2025-10-23) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [3040b777e](https://github.com/angular/angular-cli/commit/3040b777e40bc90fd1ed961e3d134875b3f9b464) | feat | add style language detection to list_projects tool | +| [45024e836](https://github.com/angular/angular-cli/commit/45024e836b4006cc48b18bb99d025ae1a572db12) | feat | add unit test framework detection to list_projects tool | +| [286b6204c](https://github.com/angular/angular-cli/commit/286b6204c825c990761a0d5e5b91bb439dd13655) | feat | make documentation search tool version-aware | +| [406315d09](https://github.com/angular/angular-cli/commit/406315d0939c62d9f4f39ce64b168e72bbdd588c) | feat | make find_examples tool version-aware | +| [68e711307](https://github.com/angular/angular-cli/commit/68e711307eae88a621698c2a9cc2abc30d44efc8) | feat | make get_best_practices tool version-aware | +| [122a8c0e2](https://github.com/angular/angular-cli/commit/122a8c0e27342db79eb4d38e23032548054709b9) | fix | correct frontmatter parsing in MCP examples tool | +| [431106559](https://github.com/angular/angular-cli/commit/431106559d6e75f5113876a3f92fdf4dc4b2114d) | fix | correct query in find_examples to prevent runtime error | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [2ffc527b1](https://github.com/angular/angular-cli/commit/2ffc527b1bbe237e9f732d3506ce3a75ca1ca9d0) | feat | configure Vitest for new projects and allow runner choice | +| [512ad282a](https://github.com/angular/angular-cli/commit/512ad282aecbfdf1e5c9e9700cc722addb949b68) | fix | preserve blank lines in jasmine-to-vitest schematic | +| [b524ba426](https://github.com/angular/angular-cli/commit/b524ba42625cd690177a300ca4843ef4edce035f) | fix | remove empty i18n-extract target for new projects | +| [54c4eae2a](https://github.com/angular/angular-cli/commit/54c4eae2aa49c6b45c41f0718a5915a10d426cb4) | fix | transform Jasmine type annotations in jasmine-to-vitest schematic | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [18bf8e7b3](https://github.com/angular/angular-cli/commit/18bf8e7b3b36b968d064299e5c557942c0ed7ec0) | fix | respect `--force` option when schematic contains `host.create` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [bf468e1eb](https://github.com/angular/angular-cli/commit/bf468e1eb1050c60359b6f52692ce0db286982c2) | fix | direct check include file exists in unit-test discovery | +| [b1d6d2f17](https://github.com/angular/angular-cli/commit/b1d6d2f174027a1f7800bd70ebd35143f92dc38c) | fix | resolve Angular locale data namespace in esbuild | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [85c18b4ea](https://github.com/angular/angular-cli/commit/85c18b4ead77c6c090b9f5c63e9e034e58369152) | fix | correctly handle routes with matrix parameters | +| [58dcfd109](https://github.com/angular/angular-cli/commit/58dcfd1094fec52102f339e8fd3b5dd2925229b9) | fix | ensure server-side navigation triggers a redirect | + + + + + +# 20.3.7 (2025-10-22) + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [a31533cf4](https://github.com/angular/angular-cli/commit/a31533cf492048f62a41b9c09e53779269ee172d) | fix | respect `--force` option when schematic contains `host.create` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [8cdda111c](https://github.com/angular/angular-cli/commit/8cdda111cc0b343aa5eb6a7ccbad93302a543226) | fix | resolve Angular locale data namespace in esbuild | +| [5847ccc54](https://github.com/angular/angular-cli/commit/5847ccc545e54eb77a78b2435db7970faf748156) | fix | update `vite` to `7.11.1` | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [3a28fb6a1](https://github.com/angular/angular-cli/commit/3a28fb6a13061215b881c49232db979fc3c2f641) | fix | correctly handle routes with matrix parameters | +| [5db6d6487](https://github.com/angular/angular-cli/commit/5db6d64870c7ce0b883722a07c828946b7d2217d) | fix | ensure server-side navigation triggers a redirect | + + + + + +# 21.0.0-next.8 (2025-10-15) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [ede5e52bc](https://github.com/angular/angular-cli/commit/ede5e52bc701c42948bd98826cd4fa901350015c) | feat | add `include` option to jasmine-to-vitest schematic | +| [d0d2a17b8](https://github.com/angular/angular-cli/commit/d0d2a17b8adb2c1ce6eee70494f5d2298622c40e) | feat | add Jasmine spy API transformations to jasmine-to-vitest schematic | +| [e7d955bed](https://github.com/angular/angular-cli/commit/e7d955bedd5ca6957903cb73f8ebe06823a808da) | feat | add matcher transformations to jasmine-to-vitest schematic | +| [629f5cb18](https://github.com/angular/angular-cli/commit/629f5cb181fee562645baf02b44ebb3b39f3fb06) | feat | add misc transformations to jasmine-to-vitest schematic | +| [58474ec7d](https://github.com/angular/angular-cli/commit/58474ec7dd85fc34639c138d9b8d545affb50e3e) | feat | introduce initial jasmine-to-vitest unit test refactor schematic | +| [8f0f6a5f1](https://github.com/angular/angular-cli/commit/8f0f6a5f113ffc9e81d99eeeba71f8054e2d3686) | fix | add migration to update `moduleResolution` to `bundler` | +| [f35b9f331](https://github.com/angular/angular-cli/commit/f35b9f3310995b05d501f2abaec58dcd283e3aa0) | fix | improve comment preservation in jasmine-to-vitest | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [53899511a](https://github.com/angular/angular-cli/commit/53899511afe5665541984085914a313390af6ce2) | fix | expand `jest` and `jest-environment-jsdom` to allow version 30 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [a90bea5b5](https://github.com/angular/angular-cli/commit/a90bea5b51c6978441919ed2af85c090fe99fd38) | feat | support `.test.ts` files by default in unit test builder | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [7be6c8f0e](https://github.com/angular/angular-cli/commit/7be6c8f0e2883c85546eb1691c91fa7d4aefc0d3) | fix | prevent malicious URL from overriding host | + + + + + +# 20.3.6 (2025-10-15) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [5271547c8](https://github.com/angular/angular-cli/commit/5271547c80662de10cb3bcb648779a83f6efedfb) | fix | prevent malicious URL from overriding host | + + + + + +# 19.2.18 (2025-10-15) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [9136a5d13](https://github.com/angular/angular-cli/commit/9136a5d1302bb224ea245460ae29474bd2a3a10b) | fix | prevent malicious URL from overriding host | + + + + + +# 21.0.0-next.7 (2025-10-08) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [1c06b16a9](https://github.com/angular/angular-cli/commit/1c06b16a962d3c2cc122dc40e01c64bc8a8d754d) | feat | add builder info to `list_projects` MCP tool | +| [104c90768](https://github.com/angular/angular-cli/commit/104c90768000b3e0052ee7e7de2c5e04c1bffdaf) | feat | enhance `ng version` output with more details | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [afb4d3e37](https://github.com/angular/angular-cli/commit/afb4d3e377b11315a03563cb8c143c35d37f113a) | fix | remove extra space before async in spec templates | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [1c2d49ec7](https://github.com/angular/angular-cli/commit/1c2d49ec736818d22773916d7eaafd3446275ea0) | fix | cleanup karma temporary directory after process exit | +| [50e330d33](https://github.com/angular/angular-cli/commit/50e330d331fc8cfc4c12f7258012305ecb419d2d) | fix | disable glob directory expansion when finding tests | +| [73621998f](https://github.com/angular/angular-cli/commit/73621998f91db189ad9b1ab006681404e30f7900) | fix | normalize paths for Vitest runner output files | + + + + + +# 20.3.5 (2025-10-08) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [7f7140680](https://github.com/angular/angular-cli/commit/7f7140680b75ff6b41f7f04349fe10cd928f1a23) | fix | cleanup karma temporary directory after process exit | + + + + + +# 21.0.0-next.6 (2025-10-03) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [58d101d5e](https://github.com/angular/angular-cli/commit/58d101d5e78cf4c158dbaf52103639d56b84f9ed) | feat | add `--json` output to `ng version` | +| [50453fdee](https://github.com/angular/angular-cli/commit/50453fdeec4a00d88deada49d2dd0867bdb784fb) | feat | overhaul `ng version` command output | +| [0922a033f](https://github.com/angular/angular-cli/commit/0922a033f546b38f83d1cae524cf7237dd37a2ac) | fix | improve JSON schema parsing for command options | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [c119910f4](https://github.com/angular/angular-cli/commit/c119910f4505e280eea83ea6647b6d279a46f36d) | feat | add AGENTS.md support to ai-config schematic | +| [9dab5780a](https://github.com/angular/angular-cli/commit/9dab5780a1befbd76ee9ba4c4e6ac2d3fd714bb9) | fix | add fixture.whenStable in spec files when zoneless apps | +| [e304821d5](https://github.com/angular/angular-cli/commit/e304821d5d789fab2725d3152612d3e5b6bd0dc7) | fix | make ai-config schematic non-destructive | +| [8ac515699](https://github.com/angular/angular-cli/commit/8ac515699cfd5a4e7eda9bcc054dfd7c68faba39) | fix | Out of the box support for PM2 | +| [57075a31a](https://github.com/angular/angular-cli/commit/57075a31ad8f95a82304fe8533b3bca828d8da42) | fix | use bracket notation for `process.env['pm_id']` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [acd785afc](https://github.com/angular/angular-cli/commit/acd785afc956efad56b03402085ff94855b9fcc6) | fix | mark `InjectionToken` as pure for improved tree-shaking | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [3e0209d0a](https://github.com/angular/angular-cli/commit/3e0209d0a6bc6d7985d6294fc1430cdbe4d2d9a6) | feat | add `browserViewport` option for vitest browser tests | +| [3b7dabbf1](https://github.com/angular/angular-cli/commit/3b7dabbf1df9b2b6ca9ffc6c038abb6e40b6df2b) | feat | add advanced coverage options to unit-test builder | +| [65562114c](https://github.com/angular/angular-cli/commit/65562114cdf725fa52f6d805f26a1aa265b9badb) | fix | mark `InjectionToken` as pure for improved tree-shaking | +| [d0787c11d](https://github.com/angular/angular-cli/commit/d0787c11d68841c36ef28bc3f15963406d1209a9) | fix | provide default excludes for vitest coverage | +| [ac10f323e](https://github.com/angular/angular-cli/commit/ac10f323ece9f4a35068e510f10786fbcb15adbb) | fix | relax requirement for files to be in TS compilation | + + + + + +# 20.3.4 (2025-10-02) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [c94bf7ff0](https://github.com/angular/angular-cli/commit/c94bf7ff0845fe325c39737057ff1ed4ea553011) | fix | Out of the box support for PM2 | +| [465436c9f](https://github.com/angular/angular-cli/commit/465436c9fa21173befe5e39b61afb7f29435c2aa) | fix | use bracket notation for `process.env['pm_id']` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [bc6b63114](https://github.com/angular/angular-cli/commit/bc6b631146c719a337c937e95c7cc5ebca29254b) | fix | mark `InjectionToken` as pure for improved tree-shaking | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [e510ff828](https://github.com/angular/angular-cli/commit/e510ff828f033478d8e1720050a7b3d75d551e16) | fix | mark `InjectionToken` as pure for improved tree-shaking | + + + + + +# 21.0.0-next.5 (2025-09-24) + +## Breaking Changes + +### @angular/build + +- The `javascriptEnabled` option for Less is no longer supported. Projects relying on inline JavaScript within Less files will need to refactor their stylesheets to remove this dependency. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [2a518016d](https://github.com/angular/angular-cli/commit/2a518016d9585dd4d16f90102d5409459ebba024) | feat | Applications are zoneless by default | +| [9f255f2b3](https://github.com/angular/angular-cli/commit/9f255f2b3cc435f3bea2f0266a137176ca599aef) | feat | set `packageManager` in `package.json` on new projects | +| [77741f5ee](https://github.com/angular/angular-cli/commit/77741f5eec735f23b0f2865101471e045e4889b8) | fix | add 'update-typescript-lib' migration | +| [3af4dcbbf](https://github.com/angular/angular-cli/commit/3af4dcbbf4019e13a9547a404516502cf4eda736) | fix | add `__screenshots__/` to `.gitignore` | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [6d3a3c579](https://github.com/angular/angular-cli/commit/6d3a3c5799bde1bab5c3878e0783ffa6854e36ad) | feat | add ai-tutor mcp tool | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [7a8c94615](https://github.com/angular/angular-cli/commit/7a8c94615164e114533fae0f84796a374dc1b47b) | fix | make zone.js optional in server and app-shell builders | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [00426e315](https://github.com/angular/angular-cli/commit/00426e3150c846913a5aa31510b5a1126df9e570) | feat | add --list-tests flag to unit-test builder | +| [3478aa332](https://github.com/angular/angular-cli/commit/3478aa332ef0241c04e7eeef9dd74b017292b2c4) | fix | exclude .angular from coverage instrumentation | +| [139758586](https://github.com/angular/angular-cli/commit/13975858683421a5712bbfccee57cf141a0b96f6) | fix | remove deprecated `javascriptEnabled` option for Less | +| [705af2278](https://github.com/angular/angular-cli/commit/705af22788102eeade08404d357582c39de8900b) | fix | set coverage report directory to coverage/project-name | +| [907eabdd3](https://github.com/angular/angular-cli/commit/907eabdd3c7447ed2c211b6d6c2719b04443c545) | fix | support ESM PostCSS plugins | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [afa273849](https://github.com/angular/angular-cli/commit/afa273849d0e0e62a7df2236d818bf7800c3ad13) | fix | avoid retaining rendered HTML in memory post-request | + + + + + +# 20.3.3 (2025-09-24) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [b7f92da78](https://github.com/angular/angular-cli/commit/b7f92da7835c14b568d07dfb3313802704f28cfd) | fix | add `__screenshots__/` to `.gitignore` | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [a4c9a2007](https://github.com/angular/angular-cli/commit/a4c9a2007ab3e33b2c97fa63f0df8f8662427031) | fix | avoid retaining rendered HTML in memory post-request | + + + + + +# 21.0.0-next.4 (2025-09-17) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [a908bf3d4](https://github.com/angular/angular-cli/commit/a908bf3d4e8a59f3546f117bcc1f12fd69ad2d0b) | feat | add 'filter' option to unit-test builder | +| [c0b00d78e](https://github.com/angular/angular-cli/commit/c0b00d78ec37426f4474f473ddf9e627a0dd23df) | feat | add reporter output file option for unit-test | +| [66dd6dd83](https://github.com/angular/angular-cli/commit/66dd6dd835d6b489e6b4be2138aa443e11bfa076) | feat | allow options for unit test reporters | +| [43fc5536f](https://github.com/angular/angular-cli/commit/43fc5536fd42694a09a7b7c25fe8c5665e3e28d3) | fix | add timestamp to bundle generation log | +| [c6176f6df](https://github.com/angular/angular-cli/commit/c6176f6dffdae5c8d8708a1dd20fb51ca72e3c24) | fix | add upfront dependency validation for unit-test runners | +| [69c3b1226](https://github.com/angular/angular-cli/commit/69c3b1226880835fd8087cea5684ababb92b1c05) | fix | improve error handling in unit-test builder | +| [dae732059](https://github.com/angular/angular-cli/commit/dae732059d17e9e374ac7635fbca9480751f70b3) | fix | serve build assets and styles in vitest | + + + + + +# 20.3.2 (2025-09-17) + + + + + +# 19.2.17 (2025-09-17) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [365d525b5](https://github.com/angular/angular-cli/commit/365d525b596b437ad0b1a74b1417eaae6aa8694e) | fix | update `vite` to `6.3.6` | + + + + + +# 20.3.1 (2025-09-11) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [be60be499](https://github.com/angular/angular-cli/commit/be60be4997ea0f7be3a4fb993f87b1bd29fc1493) | fix | add timestamp to bundle generation log | +| [d60f4e53d](https://github.com/angular/angular-cli/commit/d60f4e53d8f511d313e517161dc26eb3cc005f1c) | fix | update vite to version `7.1.5` | + + + + + +# 18.2.21 (2025-09-10) + +## Breaking Changes + +### @angular/ssr + +- The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector. + + Before: + + ```ts + const bootstrap = () => bootstrapApplication(AppComponent, config); + ``` + + After: + + ```ts + const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); + ``` + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [700e6bc01](https://github.com/angular/angular-cli/commit/700e6bc0177a3e345a88e31be22496cc3054349b) | fix | avoid extra tick in SSR builds | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [cccc91b91](https://github.com/angular/angular-cli/commit/cccc91b919b4a8365efce9ee691940e351349075) | fix | avoid extra tick in SSR dev-server builds | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [4af385201](https://github.com/angular/angular-cli/commit/4af385201bf8ba05352faec26c6efa866b69d999) | feat | introduce BootstrapContext for isolated server-side rendering | + + + + + +# 19.2.16 (2025-09-10) + +## Breaking Changes + +### @angular/ssr + +- The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector. + + Before: + + ```ts + const bootstrap = () => bootstrapApplication(AppComponent, config); + ``` -# 20.1.6 (2025-08-13) + After: -### @schematics/angular + ```ts + const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); + ``` -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | -| [584bc1d41](https://github.com/angular/angular-cli/commit/584bc1d4173e7f129aa20e829f1dfb03e1e0dc9e) | fix | add extra prettier config | -| [02b0506fd](https://github.com/angular/angular-cli/commit/02b0506fde638b89510e5a78b3d190ba60a8d6ba) | fix | correct configure the `typeSeparator` in the library schematic | +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [b0f4330a9](https://github.com/angular/angular-cli/commit/b0f4330a9a2f598b71f12d07e49b6c7c6891febd) | fix | avoid extra tick in SSR builds | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [ee5c5f823](https://github.com/angular/angular-cli/commit/ee5c5f823c87a36c9bcb92db2fc9b4e652dc16c2) | fix | avoid extra tick in SSR dev-server builds | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [32980f7e7](https://github.com/angular/angular-cli/commit/32980f7e7a5821bc9bd311dda6e134970e735722) | feat | introduce BootstrapContext for isolated server-side rendering | - + + +# 21.0.0-next.3 (2025-09-10) + +## Breaking Changes + +### @angular/build + +- - TypeScript versions older than 5.9 are no longer supported. + +### @angular/ssr + +- The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector. + + Before: + + ```ts + const bootstrap = () => bootstrapApplication(AppComponent, config); + ``` + + After: + + ```ts + const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); + ``` + +### @schematics/angular -# 20.2.0-next.3 (2025-08-08) +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [ddebe3d4f](https://github.com/angular/angular-cli/commit/ddebe3d4fc35486a57f4051fdd4493caba4e6c07) | fix | align labels in ai-config schema | +| [8e6e0a293](https://github.com/angular/angular-cli/commit/8e6e0a2931bfb178e77cf2c9ca7f92a56c673449) | fix | remove explicit flag for host bindings | +| [b983ea8e5](https://github.com/angular/angular-cli/commit/b983ea8e5107420a910dbbc05c6b74f0ff6fbddd) | fix | respect skip-install for tailwind schematic | ### @angular/cli -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | -| [51d56f770](https://github.com/angular/angular-cli/commit/51d56f770714a015aa7621d53c4a1634e8a01cc8) | fix | cache MCP best practices content and add tool annotations | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [d014630fa](https://github.com/angular/angular-cli/commit/d014630fad765ae3928b698122038cbe00d37102) | feat | add advanced filtering to MCP example search | +| [1ee9ce3c9](https://github.com/angular/angular-cli/commit/1ee9ce3c93caff419f8095a91cf58601e3df3f74) | feat | promote MCP `find_examples` tool to a stable tool | +| [dbf1aaf70](https://github.com/angular/angular-cli/commit/dbf1aaf70bc3e3dd0de05d760bafacc43b34dce8) | fix | add snippet support to example search MCP tool | +| [11cee1acb](https://github.com/angular/angular-cli/commit/11cee1acb59afbad1ef88d8340b5438f7dbefe57) | fix | correct boolean parsing in MCP example front matter | +| [def412a55](https://github.com/angular/angular-cli/commit/def412a558d71cb51fa16d826418bd0ed0a085cf) | fix | enhance find_examples MCP tool with structured output | +| [2037b912b](https://github.com/angular/angular-cli/commit/2037b912b2f78eb4469d8671fbca8c43f06cd2ff) | fix | improve bun lockfile detection and optimize lockfile checks | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [9749ec687](https://github.com/angular/angular-cli/commit/9749ec687800c1bbeae4b75550dee3608bbe6823) | fix | avoid extra tick in SSR builds | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [cd5c92b99](https://github.com/angular/angular-cli/commit/cd5c92b99a5d8e9cb991a2551f564353c3df0fbe) | fix | correct Vitest coverage reporting for test files | +| [1529595d4](https://github.com/angular/angular-cli/commit/1529595d4a8d8ff9251d1680b1a23bf4ef817db0) | fix | drop support for TypeScript 5.8 | +| [58da860fc](https://github.com/angular/angular-cli/commit/58da860fc4e040d1dbce0b1955c361a2efdb3559) | fix | preserve names in esbuild for improved debugging in dev mode | +| [26127bd3b](https://github.com/angular/angular-cli/commit/26127bd3bb2c4b9aacf2a8f4c2cbdf732512bafb) | fix | resolve PostCSS plugins relative to config file | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [f0b0980fb](https://github.com/angular/angular-cli/commit/f0b0980fbd55473f152ec3b87fa5e1923c876854) | feat | introduce BootstrapContext for isolated server-side rendering | - + -# 20.1.5 (2025-08-06) +# 20.3.0 (2025-09-10) + +## Breaking Changes + +### @angular/ssr + +- The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector. + + Before: + + ```ts + const bootstrap = () => bootstrapApplication(AppComponent, config); + ``` + + After: + + ```ts + const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); + ``` + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | +| [ef20a278d](https://github.com/angular/angular-cli/commit/ef20a278d1455b9cdffc5102b13d0b2206ef1ecb) | fix | align labels in ai-config schema | ### @angular/cli -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | -| [48ca04474](https://github.com/angular/angular-cli/commit/48ca044745f49bc7fc365a621827294f4cc82c50) | fix | cache MCP best practices content and add tool annotations | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [f6ad41c13](https://github.com/angular/angular-cli/commit/f6ad41c134c7ae938ccda908967e7cc863b3db16) | fix | improve bun lockfile detection and optimize lockfile checks | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [1a7890873](https://github.com/angular/angular-cli/commit/1a789087344aa94d061839122e6a63efbfc9c905) | fix | avoid extra tick in SSR builds | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [5d46d6ec1](https://github.com/angular/angular-cli/commit/5d46d6ec114052715a8bd17761a4f258961ad26b) | fix | preserve names in esbuild for improved debugging in dev mode | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [7eacb4187](https://github.com/angular/angular-cli/commit/7eacb41878f5fdac8d40aedfcca6794b77eda5ff) | feat | introduce BootstrapContext for isolated server-side rendering | - + -# 20.2.0-next.2 (2025-07-30) +# 21.0.0-next.2 (2025-09-03) ### @angular/cli +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [301b50da4](https://github.com/angular/angular-cli/commit/301b50da4cf99b3cd87940606121d076b4f241c6) | feat | add fallback support for packages without direct `ng add` functionality | +| [2c498d2b8](https://github.com/angular/angular-cli/commit/2c498d2b87c13a63bef2a9be2ca4f087c72c6b8a) | fix | don't set a default for array options when length is 0 | +| [f099c9157](https://github.com/angular/angular-cli/commit/f099c91570b3cd748d7138bd18a4898a345549db) | fix | improve list_projects MCP tool to find all workspaces in monorepos | +| [e192e8c7e](https://github.com/angular/angular-cli/commit/e192e8c7ecf506e4b03668f527de83f2a57f552d) | fix | set process title when running architect commands | + +### @schematics/angular + | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | -| [193b39416](https://github.com/angular/angular-cli/commit/193b39416731fa439fea7da8c06d5d287df99bc1) | fix | skip workspace-specific tools when outside a workspace | +| [e417c89f9](https://github.com/angular/angular-cli/commit/e417c89f9e9cfe0ce50ffbc72ef555793605aea1) | feat | Add `addTypeToClassName` option to relevant schematics | +| [4e6c94f21](https://github.com/angular/angular-cli/commit/4e6c94f21e882c593cf11197900c29d693af9297) | feat | support different file name style guides in `ng new` | +| [14c0a9bac](https://github.com/angular/angular-cli/commit/14c0a9bacbb66b1db714ea7906c7d33f49c710fc) | perf | optimize AST traversal utilities | ### @angular/build -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | -| [7a183730c](https://github.com/angular/angular-cli/commit/7a183730c77689fb9e63625f5ef20aef1cefb88b) | fix | skip vite transformation of CSS-like assets | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [7b0f69798](https://github.com/angular/angular-cli/commit/7b0f69798f061d5500620828cf304e05d667199f) | fix | avoid extra tick in SSR dev-server builds | +| [f806f6477](https://github.com/angular/angular-cli/commit/f806f6477af222907f1879181fb0f9839e889ea8) | fix | maintain media output hashing with vitest unit-testing | - + -# 20.1.4 (2025-07-30) +# 20.2.2 (2025-09-03) ### @angular/cli | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | -| [2d753cc62](https://github.com/angular/angular-cli/commit/2d753cc62c9a801c40923a43e4af5f74b22700e0) | fix | skip workspace-specific tools when outside a workspace | +| [a793bbc47](https://github.com/angular/angular-cli/commit/a793bbc473dfaddf3fe6ed15805dc4fc84f52865) | fix | don't set a default for array options when length is 0 | +| [2736599e2](https://github.com/angular/angular-cli/commit/2736599e2f6c61032810d8e336c1646db4066392) | fix | set process title when running architect commands | ### @angular/build -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | -| [42d72ef4d](https://github.com/angular/angular-cli/commit/42d72ef4d99380dbb1c0e03e3e3abfb2223fa539) | fix | skip vite transformation of CSS-like assets | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [5c2abffea](https://github.com/angular/angular-cli/commit/5c2abffea6cf3f672ee256a944dba56dd257665b) | fix | avoid extra tick in SSR dev-server builds | +| [f3c826853](https://github.com/angular/angular-cli/commit/f3c826853501c9cf6d07a1c8ee3363eb79f53005) | fix | maintain media output hashing with vitest unit-testing | - + -# 20.1.3 (2025-07-24) +# 21.0.0-next.1 (2025-08-27) + +## Breaking Changes + +### @angular/cli + +- The `ng` commands will no longer automatically detect and use `cnpm` as the package manager. As an alternative use the `.npmrc` file to ensure npm uses the cnpm registry. + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- | +| [aed26c388](https://github.com/angular/angular-cli/commit/aed26c38803a465842ff128c3f81bd6984e1fe3d) | fix | correctly set default array values | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [4912f3990](https://github.com/angular/angular-cli/commit/4912f39906b11a3212f11d5a00d577e2a0bacab4) | feat | add Tailwind CSS option to application schematic and `ng new` | +| [6c7b79833](https://github.com/angular/angular-cli/commit/6c7b798332786d29070460669e093e37902c4438) | fix | directly resolve karma config template in migration | +| [0f86cf878](https://github.com/angular/angular-cli/commit/0f86cf8782d1c08d11bb9ee54a30fe1954dd8bcc) | fix | prevent AI config schematic from failing when 'none' and other AI tools are selected | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [0d53e82d5](https://github.com/angular/angular-cli/commit/0d53e82d5ed8986603c2005fc06041dd076b08c6) | feat | provide detailed peer dependency conflict errors in ng add | +| [f513089e2](https://github.com/angular/angular-cli/commit/f513089e276acf5a7c4f6879a95e2d6ed78ae67d) | feat | remove direct support for `cnpm` | +| [47d77a3ed](https://github.com/angular/angular-cli/commit/47d77a3edea4dabb463d50c2bdba32475257d775) | fix | correctly set default array values | +| [e5aed6d65](https://github.com/angular/angular-cli/commit/e5aed6d655ed92ea6eb3ac03716b8a02a5f731d6) | fix | show planned actions in `ng add` dry run | +| [aeb49dd52](https://github.com/angular/angular-cli/commit/aeb49dd52bf88785a193fcb6caa0b36aaeef1d37) | perf | cache dependency lookups during `ng add` | +| [5e534090e](https://github.com/angular/angular-cli/commit/5e534090e25e00a9fafbce2867030e7fdb0efbf6) | perf | parallelize peer dependency checks in `ng add` | ### @angular/build -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | -| [ea5cd0e81](https://github.com/angular/angular-cli/commit/ea5cd0e81196467ea66f50c106cffec1cd8a1a56) | fix | update `vite` to `7.0.6` | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [b554bd73a](https://github.com/angular/angular-cli/commit/b554bd73a9c248d986ed718028edf52ab5da6ccf) | fix | add temporary directory cleanup for Vitest executor | +| [261dbb37c](https://github.com/angular/angular-cli/commit/261dbb37cbe01492240c4cedc644663b15a4296a) | fix | correct JS/TS file paths when running under Bazel | +| [abf003268](https://github.com/angular/angular-cli/commit/abf003268c6cb18f0944665b0b3f2794c9469c3e) | fix | correct Vitest builder watch mode execution | +| [4b49a207a](https://github.com/angular/angular-cli/commit/4b49a207a1de27b82416c6225a59bc10f48bdcbc) | fix | ensure karma polyfills reporter factory returns a value | - + -# 20.2.0-next.1 (2025-07-23) +# 20.2.1 (2025-08-27) ### @angular/cli -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | -| [fefa7a46f](https://github.com/angular/angular-cli/commit/fefa7a46f5733fd77852a61fddc3120b1bb4b202) | fix | `define` option is being included multiple times in the JSON help | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- | +| [3b693e09e](https://github.com/angular/angular-cli/commit/3b693e09e8148ef22031aab8f6bc70c928aabc03) | fix | correctly set default array values | -### @angular-devkit/core +### @schematics/angular -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | -| [7595e1f88](https://github.com/angular/angular-cli/commit/7595e1f8887bafd344ec939e647e3fca8bbd98be) | fix | use crypto.randomUUID instead of Date.now for unique string in tmp file names | +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [6937123a3](https://github.com/angular/angular-cli/commit/6937123a393e2ba9221962b0174056c14437a988) | fix | directly resolve karma config template in migration | +| [5d6dd4425](https://github.com/angular/angular-cli/commit/5d6dd44259a0d89098c2a0c784e726b43ce32316) | fix | prevent AI config schematic from failing when 'none' and other AI tools are selected | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- | +| [e93919dea](https://github.com/angular/angular-cli/commit/e93919dea7df55a3aac2fa5c93c4560c50a2d749) | fix | correctly set default array values | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [06a6ddc10](https://github.com/angular/angular-cli/commit/06a6ddc102f5dc9018ec982f6e4cf56259cc4b52) | fix | correct JS/TS file paths when running under Bazel | +| [b6816b0cb](https://github.com/angular/angular-cli/commit/b6816b0cbaf1262d7015b9d7f7fb425f53995947) | fix | ensure karma polyfills reporter factory returns a value | + + + + + +# 21.0.0-next.0 (2025-08-20) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [0505f954d](https://github.com/angular/angular-cli/commit/0505f954dcf3b3339749ff461592d46d8ecc5e23) | fix | allow unit-test progress option passthrough for building | + + + + + +# 20.2.0 (2025-08-20) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [b4de9a1bf](https://github.com/angular/angular-cli/commit/b4de9a1bf50a35404fb79eb3f120faafd0ce825a) | feat | add --experimental-tool option to mcp command | +| [755ba70fd](https://github.com/angular/angular-cli/commit/755ba70fd7ef38793d15797ba402020c375c3295) | feat | add --local-only option to mcp command | +| [59d7ef343](https://github.com/angular/angular-cli/commit/59d7ef343b6f1feea37a019935578c560d3d5e41) | feat | add --read-only option to mcp command | +| [4e92eb6f1](https://github.com/angular/angular-cli/commit/4e92eb6f17cb30259bc8e8d1979bbd9989bc5ad0) | feat | add modernize tool to the MCP server | +| [a3b25f675](https://github.com/angular/angular-cli/commit/a3b25f675283fdd8cc5689e3ec88f27aa1386390) | fix | add choices to command line parser when type is array and has an enum | +| [e19eee614](https://github.com/angular/angular-cli/commit/e19eee61404a9ca6268ebbc69f671a422d81df9b) | fix | address Node.js deprecation DEP0190 | +| [4ee6f327a](https://github.com/angular/angular-cli/commit/4ee6f327a206f8ff2ad5eeab43193df56b92b5e0) | fix | apply default to array types | +| [8ba6b0bcc](https://github.com/angular/angular-cli/commit/8ba6b0bcc8c8087875d14a0aefc6b7b52f39ce2a) | fix | use correct path for MCP get_best_practices tool | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [2e3cfd598](https://github.com/angular/angular-cli/commit/2e3cfd598c9366d0036a52cd18024317b33e6fca) | feat | add migration to remove default Karma configurations | +| [d80dae276](https://github.com/angular/angular-cli/commit/d80dae276e9554c13e0c37640d0db8acafc9d48b) | feat | add schematics to generate ai context files. | +| [ffe6fb916](https://github.com/angular/angular-cli/commit/ffe6fb916d496da1c6c20942f6e6b05a679b0f7d) | fix | allow AI config prompt to be skipped without selecting a value | +| [ae2802b7d](https://github.com/angular/angular-cli/commit/ae2802b7db358c5a3f0590feea212a768a710353) | fix | improve AI config prompt wording | +| [b017f84fd](https://github.com/angular/angular-cli/commit/b017f84fdaf36bc0fcad2241846665c73b52b6d8) | fix | improve coverage directory handling for Karma configuration comparisons | +| [6a79f9a75](https://github.com/angular/angular-cli/commit/6a79f9a75cdcbb0761c4044066748f4eb788a57f) | fix | zoneless is now stable | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------- | +| [c43504d8d](https://github.com/angular/angular-cli/commit/c43504d8d96a4436ce71c23d957aec2d080106b8) | fix | address Node.js deprecation DEP0190 | ### @angular/build @@ -107,6 +861,61 @@ + + +# 20.1.6 (2025-08-13) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [584bc1d41](https://github.com/angular/angular-cli/commit/584bc1d4173e7f129aa20e829f1dfb03e1e0dc9e) | fix | add extra prettier config | +| [02b0506fd](https://github.com/angular/angular-cli/commit/02b0506fde638b89510e5a78b3d190ba60a8d6ba) | fix | correct configure the `typeSeparator` in the library schematic | + + + + + +# 20.1.5 (2025-08-06) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [48ca04474](https://github.com/angular/angular-cli/commit/48ca044745f49bc7fc365a621827294f4cc82c50) | fix | cache MCP best practices content and add tool annotations | + + + + + +# 20.1.4 (2025-07-30) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [2d753cc62](https://github.com/angular/angular-cli/commit/2d753cc62c9a801c40923a43e4af5f74b22700e0) | fix | skip workspace-specific tools when outside a workspace | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [42d72ef4d](https://github.com/angular/angular-cli/commit/42d72ef4d99380dbb1c0e03e3e3abfb2223fa539) | fix | skip vite transformation of CSS-like assets | + + + + + +# 20.1.3 (2025-07-24) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [ea5cd0e81](https://github.com/angular/angular-cli/commit/ea5cd0e81196467ea66f50c106cffec1cd8a1a56) | fix | update `vite` to `7.0.6` | + + + # 20.1.2 (2025-07-23) @@ -125,12 +934,6 @@ - - -# 20.2.0-next.0 (2025-07-16) - - - # 20.1.1 (2025-07-16) @@ -4222,6 +5025,7 @@ Alan Agius, Charles Lyding, Doug Parker, Joey Perrott and Piotr Wysocki ```scss @import 'font-awesome/scss/font-awesome'; ``` + - By default the CLI will use Sass modern API, While not recommended, users can still opt to use legacy API by setting `NG_BUILD_LEGACY_SASS=1`. - Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0642e0b7ff65..06db9756c89d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -294,22 +294,10 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise [coc]: https://github.com/angular/code-of-conduct/blob/main/CODE_OF_CONDUCT.md [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# [corporate-cla]: https://code.google.com/legal/corporate-cla-v1.0.html -[dev-doc]: https://github.com/angular/angular-cli/blob/main/packages/angular/cli/README.md#development-hints-for-working-on-angular-cli +[dev-doc]: https://github.com/angular/angular-cli/blob/main/docs/DEVELOPER.md [GitHub]: https://github.com/angular/angular-cli [gitter]: https://gitter.im/angular/angular-cli [individual-cla]: https://code.google.com/legal/individual-cla-v1.0.html [js-style-guide]: https://google.github.io/styleguide/jsguide.html [stackoverflow]: https://stackoverflow.com/questions/tagged/angular-devkit -## Updating the Public API -Our Public API surface is tracked using golden files. - -You check all golden files by running: -```bash -pnpm public-api:check -``` - -If you modified the public API, the test will fail. To update the golden files you need to run: -```bash -pnpm public-api:update -``` diff --git a/MODULE.bazel b/MODULE.bazel index f90ed9010d4c..696be6bbf297 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,5 +1,175 @@ -# TODO(devversion): Investigate bzlmod and use it where possible. +"""Rules/toolchains for angular_cli with Bazel.""" module( name = "angular_cli", ) + +bazel_dep(name = "yq.bzl", version = "0.3.1") +bazel_dep(name = "rules_nodejs", version = "6.6.0") +bazel_dep(name = "aspect_rules_js", version = "2.8.0") +bazel_dep(name = "aspect_rules_ts", version = "3.7.1") +bazel_dep(name = "rules_pkg", version = "0.8.1") + +# Alow for usage of rules_pkg@0.8.1 even though other deps want a later verison. +multiple_version_override( + module_name = "rules_pkg", + versions = [ + "0.8.1", + "1.1.0", + ], +) + +bazel_dep(name = "aspect_bazel_lib", version = "2.21.2") +bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "aspect_rules_esbuild", version = "0.24.0") +bazel_dep(name = "aspect_rules_jasmine", version = "2.0.0") +bazel_dep(name = "rules_angular") +git_override( + module_name = "rules_angular", + commit = "059242464577ca9046451afebd6734ec79908d6f", + remote = "https://github.com/devversion/rules_angular.git", +) + +bazel_dep(name = "devinfra") +git_override( + module_name = "devinfra", + commit = "75d22eaeab72c5f03da29038c3f367a36c42017a", + remote = "https://github.com/angular/dev-infra.git", +) + +bazel_dep(name = "rules_sass") +git_override( + module_name = "rules_sass", + commit = "1184a80751a21af8348f308abc5b38a41f26850e", + remote = "https://github.com/devversion/rules_sass.git", +) + +bazel_dep(name = "rules_browsers") +git_override( + module_name = "rules_browsers", + commit = "6a699bf3e896690e2923cf3ade29fbd4e492e366", + remote = "https://github.com/devversion/rules_browsers.git", +) + +node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node") +node.toolchain(node_version = "24.0.0") +use_repo( + node, + "nodejs_darwin_amd64", + "nodejs_darwin_arm64", + "nodejs_linux_amd64", + "nodejs_linux_arm64", + "nodejs_toolchains", + "nodejs_windows_amd64", +) + +node_dev = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node", dev_dependency = True) + +# Node.js 20 +node_dev.toolchain( + name = "node20", + node_version = "20.19.0", +) + +# Node.js 22 +node_dev.toolchain( + name = "node22", + node_version = "22.12.0", +) + +# Node.js 24 +node_dev.toolchain( + name = "node24", + node_version = "24.0.0", +) +use_repo( + node_dev, + "node20_darwin_amd64", + "node20_darwin_arm64", + "node20_linux_amd64", + "node20_linux_arm64", + "node20_toolchains", + "node20_windows_amd64", + "node22_darwin_amd64", + "node22_darwin_arm64", + "node22_linux_amd64", + "node22_linux_arm64", + "node22_toolchains", + "node22_windows_amd64", + "node24_darwin_amd64", + "node24_darwin_arm64", + "node24_linux_amd64", + "node24_linux_arm64", + "node24_toolchains", + "node24_windows_amd64", +) + +# This is needed following https://github.com/bazel-contrib/rules_nodejs/pull/3859 +register_toolchains("//:node22_windows_no_exec_config_toolchain") + +npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm") +npm.npm_translate_lock( + name = "npm", + custom_postinstalls = { + # TODO: Standardize browser management for `rules_js` + "webdriver-manager": "node ./bin/webdriver-manager update --standalone false --gecko false --versions.chrome 106.0.5249.21", + }, + data = [ + "//:package.json", + "//:pnpm-workspace.yaml", + "//modules/testing/builder:package.json", + "//packages/angular/build:package.json", + "//packages/angular/cli:package.json", + "//packages/angular/create/package.json", + "//packages/angular/pwa:package.json", + "//packages/angular/ssr:package.json", + "//packages/angular_devkit/architect:package.json", + "//packages/angular_devkit/architect_cli:package.json", + "//packages/angular_devkit/build_angular:package.json", + "//packages/angular_devkit/build_webpack:package.json", + "//packages/angular_devkit/core:package.json", + "//packages/angular_devkit/schematics:package.json", + "//packages/angular_devkit/schematics_cli:package.json", + "//packages/ngtools/webpack:package.json", + "//packages/schematics/angular:package.json", + "//tests:package.json", + ], + lifecycle_hooks_envs = { + # TODO: Standardize browser management for `rules_js` + "puppeteer": ["PUPPETEER_DOWNLOAD_PATH=./downloads"], + }, + lifecycle_hooks_execution_requirements = { + # Needed for downloading chromedriver. + # Also `update-config` of webdriver manager would store an absolute path; + # which would then break execution. + "webdriver-manager": ["local"], + }, + npmrc = "//:.npmrc", + pnpm_lock = "//:pnpm-lock.yaml", + verify_node_modules_ignored = "//:.bazelignore", +) +use_repo(npm, "npm") + +rules_ts_ext = use_extension("@aspect_rules_ts//ts:extensions.bzl", "ext") +rules_ts_ext.deps( + name = "angular_cli_npm_typescript", + # Obtained by: npm info typescript@5.9.3 dist.integrity + ts_integrity = "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + ts_version = "5.9.3", +) +use_repo(rules_ts_ext, **{"npm_typescript": "angular_cli_npm_typescript"}) + +rules_angular = use_extension("@rules_angular//setup:extensions.bzl", "rules_angular") +rules_angular.setup( + name = "components_rules_angular_configurable_deps", + angular_compiler_cli = "//:node_modules/@angular/compiler-cli", + typescript = "//:node_modules/typescript", +) +use_repo(rules_angular, **{"rules_angular_configurable_deps": "components_rules_angular_configurable_deps"}) + +register_toolchains( + "@devinfra//bazel/git-toolchain:git_linux_toolchain", + "@devinfra//bazel/git-toolchain:git_macos_x86_toolchain", + "@devinfra//bazel/git-toolchain:git_macos_arm64_toolchain", + "@devinfra//bazel/git-toolchain:git_windows_toolchain", +) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 3137c9f1d3fc..30bafcb05d7c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -4,105 +4,4661 @@ "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", - "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/source.json": "7e3a9adf473e9af076ae485ed649d5641ad50ec5c11718103f34de03170d94ad", - "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef", - "https://bcr.bazel.build/modules/apple_support/1.5.0/source.json": "eb98a7627c0bc486b57f598ad8da50f6625d974c8f723e9ea71bd39f709c9862", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", + "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/1.23.1/source.json": "d888b44312eb0ad2c21a91d026753f330caa48a25c9b2102fae75eb2b0dcfdd2", + "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.16.0/MODULE.bazel": "852f9ebbda017572a7c113a2434592dd3b2f55cd9a0faea3d4be5a09a59e4900", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.21.2/MODULE.bazel": "276347663a25b0d5bd6cad869252bea3e160c4d980e764b15f3bae7f80b30624", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.21.2/source.json": "f42051fa42629f0e59b7ac2adf0a55749144b11f1efcd8c697f0ee247181e526", + "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_bazel_lib/2.9.3/MODULE.bazel": "66baf724dbae7aff4787bf2245cc188d50cb08e07789769730151c0943587c14", + "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.24.0/MODULE.bazel": "15d19e46ec74e9e49ddf3c335e7a91b0657571654b0960bdcd10b771eeb4f7cb", + "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.24.0/source.json": "6cc8c0ba6c623527e383acfe4ee73f290eeead2431093668db3b7a579a948deb", + "https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.0/MODULE.bazel": "071d1952527721bf8b180e1299def24edaece9d7466e31a311981640da82c6be", + "https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.0/source.json": "45fa9603cdfe100575a12d8b65fa425fe8713dd8c9f0cdf802168b670bc0e299", + "https://bcr.bazel.build/modules/aspect_rules_js/2.0.0/MODULE.bazel": "b45b507574aa60a92796e3e13c195cd5744b3b8aff516a9c0cb5ae6a048161c5", + "https://bcr.bazel.build/modules/aspect_rules_js/2.4.2/MODULE.bazel": "0d01db38b96d25df7ed952a5e96eac4b3802723d146961974bf020f6dd07591d", + "https://bcr.bazel.build/modules/aspect_rules_js/2.6.2/MODULE.bazel": "ed2a871f4ab8fbde0cab67c425745069d84ea64b64313fa1a2954017326511f5", + "https://bcr.bazel.build/modules/aspect_rules_js/2.7.0/MODULE.bazel": "ac879ee86f124c827e4e87942b3797ff4aaf90360eb9d7bff5321fc45d5ebefb", + "https://bcr.bazel.build/modules/aspect_rules_js/2.8.0/MODULE.bazel": "b2e0576866a3f1cca3286ad1efefa4099a6546a3239dffa802a551521e8fbf3d", + "https://bcr.bazel.build/modules/aspect_rules_js/2.8.0/source.json": "5e68a29bef5b3609a60f2b3f7be02023d6ad8224c3501cc1b51ba66791f2f332", + "https://bcr.bazel.build/modules/aspect_rules_ts/3.6.3/MODULE.bazel": "d09db394970f076176ce7bab5b5fa7f0d560fd4f30b8432ea5e2c2570505b130", + "https://bcr.bazel.build/modules/aspect_rules_ts/3.7.0/MODULE.bazel": "5aace216caf88638950ef061245d23c36f57c8359e56e97f02a36f70bb09c50f", + "https://bcr.bazel.build/modules/aspect_rules_ts/3.7.1/MODULE.bazel": "cbed416847e2c46c4c0fe275e3a3c8e302d236d0fb04a094e9af82d14e7c5040", + "https://bcr.bazel.build/modules/aspect_rules_ts/3.7.1/source.json": "7914a860fdf6ac399a3142fee2579184f0810fe0b7dee2eb9216ab72e6d8864e", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.3/MODULE.bazel": "20f53b145f40957a51077ae90b37b7ce83582a1daf9350349f0f86179e19dd0d", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.6/MODULE.bazel": "cafb8781ad591bc57cc765dca5fefab08cf9f65af363d162b79d49205c7f8af7", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/source.json": "786cbc49377fb6bf4859aec5b1c61f8fc26b08e9fdb929e2dde2e1e2a406bd24", + "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", - "https://bcr.bazel.build/modules/bazel_features/1.11.0/source.json": "c9320aa53cd1c441d24bd6b716da087ad7e4ff0d9742a9884587596edfe53015", + "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", + "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", + "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.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e", + "https://bcr.bazel.build/modules/bazel_features/1.34.0/source.json": "dfa5c4b01110313153b484a735764d247fee5624bbab63d25289e43b151a657a", + "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/source.json": "72bfbe19a3936675719157798de64631e9ac54c2b41f13b544b821d094f4840a", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.0/MODULE.bazel": "2ab127ef8d56a739a99bb2ce00ec4c7d1ecc7977d4370c0ca6efd0d8f03d6d99", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", - "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/source.json": "082ed5f9837901fada8c68c2f3ddc958bb22b6d654f71dd73f3df30d45d4b749", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", + "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", + "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", - "https://bcr.bazel.build/modules/googletest/1.11.0/source.json": "c73d9ef4268c91bd0c1cd88f1f9dfa08e814b1dbe89b5f594a9f08ba0244d206", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4", + "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.1.0/source.json": "746bf13cac0860f091df5e4911d0c593971cd8796b5ad4e809b2f8e133eee3d5", + "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", + "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", + "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73", + "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", + "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", "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/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", - "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b", + "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", + "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", + "https://bcr.bazel.build/modules/protobuf/29.0-rc3/source.json": "c16a6488fb279ef578da7098e605082d72ed85fc8d843eaae81e7d27d0f4625d", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022", + "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", + "https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4", + "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", + "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", + "https://bcr.bazel.build/modules/rules_cc/0.0.11/MODULE.bazel": "9f249c5624a4788067b96b8b896be10c7e8b4375dc46f6d8e1e51100113e0992", + "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", + "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", - "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/source.json": "d61627377bd7dd1da4652063e368d9366fc9a73920bfa396798ad92172cf645c", + "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", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", + "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", + "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", + "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", + "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", + "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", + "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", - "https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d", + "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", + "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", + "https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", - "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/source.json": "a075731e1b46bc8425098512d038d416e966ab19684a10a34f4741295642fc35", + "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", + "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", - "https://bcr.bazel.build/modules/rules_license/0.0.7/source.json": "355cc5737a0f294e560d52b1b7a6492d4fff2caf0bef1a315df5a298fca2d34a", + "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", + "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e", + "https://bcr.bazel.build/modules/rules_nodejs/6.3.0/MODULE.bazel": "45345e4aba35dd6e4701c1eebf5a4e67af4ed708def9ebcdc6027585b34ee52d", + "https://bcr.bazel.build/modules/rules_nodejs/6.5.0/MODULE.bazel": "546d0cf79f36f9f6e080816045f97234b071c205f4542e3351bd4424282a8810", + "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d", + "https://bcr.bazel.build/modules/rules_nodejs/6.6.0/MODULE.bazel": "49ef4cccc17a5a13c9beca2d0b3f7dea1d97381df8cb6ba4dea03943f6a81b0a", + "https://bcr.bazel.build/modules/rules_nodejs/6.6.0/source.json": "cbd156fa2db33707275de138110b78eb86a4cf510b979df7c4b6a8e7127484de", "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", - "https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c", + "https://bcr.bazel.build/modules/rules_pkg/0.8.1/MODULE.bazel": "7e9e7b5b26bd7ff012dfe63930db2f0176ddcd25e44a858fc72d63e995b6aab9", + "https://bcr.bazel.build/modules/rules_pkg/0.8.1/source.json": "15dd7e13dc303f7fcde2b55300bcb8de5c0dd08a7a7269749cbbaa0fb1dfbe16", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", + "https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453", + "https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608", "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", - "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9", + "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", + "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", - "https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014", + "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", + "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", + "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", + "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", + "https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917", + "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", + "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", + "https://bcr.bazel.build/modules/rules_shell/0.4.1/source.json": "4757bd277fe1567763991c4425b483477bb82e35e777a56fd846eb5cceda324a", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", - "https://bcr.bazel.build/modules/stardoc/0.5.1/source.json": "a96f95e02123320aa015b956f29c00cb818fa891ef823d55148e1a362caacf29", + "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.5.4/MODULE.bazel": "6569966df04610b8520957cb8e97cf2e9faac2c0309657c537ab51c16c18a2a4", + "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", + "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd", + "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", + "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.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", + "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", + "https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c", + "https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459", + "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", + "https://bcr.bazel.build/modules/yq.bzl/0.2.0/MODULE.bazel": "6f3a675677db8885be4d607fde14cc51829715e3a879fb016eb9bf336786ce6d", + "https://bcr.bazel.build/modules/yq.bzl/0.3.1/MODULE.bazel": "9bcb7151b3cd4681b89d350530eaf7b45e32a44dda94843b8932b0cb1cd4594a", + "https://bcr.bazel.build/modules/yq.bzl/0.3.1/source.json": "f0b0f204a2a6b0e34b4c9541efe8c04f2ef1af65948daa784eccea738b21dbd2", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d" + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d", + "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" }, "selectedYankedVersions": {}, "moduleExtensions": { - "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": { + "@@aspect_rules_esbuild~//esbuild:extensions.bzl%esbuild": { "general": { - "bzlTransitiveDigest": "PjIds3feoYE8SGbbIq2SFTZy3zmxeO2tQevJZNDo7iY=", - "usagesDigest": "+hz7IHWN6A1oVJJWNDB6yZRG+RYhF76wAYItpAeIUIg=", + "bzlTransitiveDigest": "YvkJWukIM5Ev6xNW6ySjp6cE2bfSLKke99BALBMT8ro=", + "usagesDigest": "w3kRc6iou9hC6M+vwECvdfk0P8nsVNjHSl4Ftru44zU=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, "generatedRepoSpecs": { - "local_config_apple_cc_toolchains": { - "bzlFile": "@@apple_support~//crosstool:setup.bzl", - "ruleClassName": "_apple_cc_autoconf_toolchains", + "esbuild_darwin-x64": { + "bzlFile": "@@aspect_rules_esbuild~//esbuild:repositories.bzl", + "ruleClassName": "esbuild_repositories", + "attributes": { + "esbuild_version": "0.19.9", + "integrity": "", + "platform": "darwin-x64" + } + }, + "esbuild_darwin-arm64": { + "bzlFile": "@@aspect_rules_esbuild~//esbuild:repositories.bzl", + "ruleClassName": "esbuild_repositories", + "attributes": { + "esbuild_version": "0.19.9", + "integrity": "", + "platform": "darwin-arm64" + } + }, + "esbuild_linux-x64": { + "bzlFile": "@@aspect_rules_esbuild~//esbuild:repositories.bzl", + "ruleClassName": "esbuild_repositories", + "attributes": { + "esbuild_version": "0.19.9", + "integrity": "", + "platform": "linux-x64" + } + }, + "esbuild_linux-arm64": { + "bzlFile": "@@aspect_rules_esbuild~//esbuild:repositories.bzl", + "ruleClassName": "esbuild_repositories", + "attributes": { + "esbuild_version": "0.19.9", + "integrity": "", + "platform": "linux-arm64" + } + }, + "esbuild_win32-x64": { + "bzlFile": "@@aspect_rules_esbuild~//esbuild:repositories.bzl", + "ruleClassName": "esbuild_repositories", + "attributes": { + "esbuild_version": "0.19.9", + "integrity": "", + "platform": "win32-x64" + } + }, + "esbuild_toolchains": { + "bzlFile": "@@aspect_rules_esbuild~//esbuild/private:toolchains_repo.bzl", + "ruleClassName": "toolchains_repo", + "attributes": { + "esbuild_version": "0.19.9", + "user_repository_name": "esbuild" + } + }, + "npm__esbuild_0.19.9": { + "bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl", + "ruleClassName": "npm_import_rule", + "attributes": { + "package": "esbuild", + "version": "0.19.9", + "root_package": "", + "link_workspace": "", + "link_packages": {}, + "integrity": "sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==", + "url": "", + "commit": "", + "patch_args": [ + "-p0" + ], + "patches": [], + "custom_postinstall": "", + "npm_auth": "", + "npm_auth_basic": "", + "npm_auth_username": "", + "npm_auth_password": "", + "lifecycle_hooks": [], + "extra_build_content": "", + "generate_bzl_library_targets": false, + "extract_full_archive": false, + "exclude_package_contents": [] + } + }, + "npm__esbuild_0.19.9__links": { + "bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl", + "ruleClassName": "npm_import_links", + "attributes": { + "package": "esbuild", + "version": "0.19.9", + "dev": false, + "root_package": "", + "link_packages": {}, + "deps": {}, + "transitive_closure": {}, + "lifecycle_build_target": false, + "lifecycle_hooks_env": [], + "lifecycle_hooks_execution_requirements": [ + "no-sandbox" + ], + "lifecycle_hooks_use_default_shell_env": false, + "bins": {}, + "package_visibility": [ + "//visibility:public" + ], + "replace_package": "", + "exclude_package_contents": [] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_bazel_lib~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "aspect_bazel_lib~", + "bazel_tools", + "bazel_tools" + ], + [ + "aspect_bazel_lib~", + "tar.bzl", + "tar.bzl~" + ], + [ + "aspect_rules_esbuild~", + "aspect_rules_js", + "aspect_rules_js~" + ], + [ + "aspect_rules_esbuild~", + "aspect_tools_telemetry_report", + "aspect_tools_telemetry~~telemetry~aspect_tools_telemetry_report" + ], + [ + "aspect_rules_esbuild~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "aspect_rules_js~", + "aspect_bazel_lib", + "aspect_bazel_lib~" + ], + [ + "aspect_rules_js~", + "aspect_rules_js", + "aspect_rules_js~" + ], + [ + "aspect_rules_js~", + "aspect_tools_telemetry_report", + "aspect_tools_telemetry~~telemetry~aspect_tools_telemetry_report" + ], + [ + "aspect_rules_js~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "aspect_rules_js~", + "bazel_tools", + "bazel_tools" + ], + [ + "tar.bzl~", + "aspect_bazel_lib", + "aspect_bazel_lib~" + ], + [ + "tar.bzl~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "tar.bzl~", + "tar.bzl", + "tar.bzl~" + ] + ] + } + }, + "@@aspect_rules_js~//npm:extensions.bzl%pnpm": { + "general": { + "bzlTransitiveDigest": "mweOMQTBuXWZta9QVVcvPXd9OFG60cwYkTOTetZwtAA=", + "usagesDigest": "e5/bEO6uDxa5C/p576W7VqaXk+nzIKZvtUFWD9kTD9o=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "pnpm": { + "bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl", + "ruleClassName": "npm_import_rule", + "attributes": { + "package": "pnpm", + "version": "9.15.9", + "root_package": "", + "link_workspace": "", + "link_packages": {}, + "integrity": "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==", + "url": "", + "commit": "", + "patch_args": [ + "-p0" + ], + "patches": [], + "custom_postinstall": "", + "npm_auth": "", + "npm_auth_basic": "", + "npm_auth_username": "", + "npm_auth_password": "", + "lifecycle_hooks": [], + "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])", + "generate_bzl_library_targets": false, + "extract_full_archive": true, + "exclude_package_contents": [] + } + }, + "pnpm__links": { + "bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl", + "ruleClassName": "npm_import_links", + "attributes": { + "package": "pnpm", + "version": "9.15.9", + "dev": false, + "root_package": "", + "link_packages": {}, + "deps": {}, + "transitive_closure": {}, + "lifecycle_build_target": false, + "lifecycle_hooks_env": [], + "lifecycle_hooks_execution_requirements": [ + "no-sandbox" + ], + "lifecycle_hooks_use_default_shell_env": false, + "bins": {}, + "package_visibility": [ + "//visibility:public" + ], + "replace_package": "", + "exclude_package_contents": [] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_bazel_lib~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "aspect_bazel_lib~", + "bazel_tools", + "bazel_tools" + ], + [ + "aspect_bazel_lib~", + "tar.bzl", + "tar.bzl~" + ], + [ + "aspect_rules_js~", + "aspect_bazel_lib", + "aspect_bazel_lib~" + ], + [ + "aspect_rules_js~", + "aspect_rules_js", + "aspect_rules_js~" + ], + [ + "aspect_rules_js~", + "aspect_tools_telemetry_report", + "aspect_tools_telemetry~~telemetry~aspect_tools_telemetry_report" + ], + [ + "aspect_rules_js~", + "bazel_features", + "bazel_features~" + ], + [ + "aspect_rules_js~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "aspect_rules_js~", + "bazel_tools", + "bazel_tools" + ], + [ + "bazel_features~", + "bazel_features_globals", + "bazel_features~~version_extension~bazel_features_globals" + ], + [ + "bazel_features~", + "bazel_features_version", + "bazel_features~~version_extension~bazel_features_version" + ], + [ + "tar.bzl~", + "aspect_bazel_lib", + "aspect_bazel_lib~" + ], + [ + "tar.bzl~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "tar.bzl~", + "tar.bzl", + "tar.bzl~" + ] + ] + } + }, + "@@aspect_rules_ts~//ts:extensions.bzl%ext": { + "general": { + "bzlTransitiveDigest": "pEu5+6q07zdUqscbVkhWTNLGT9OGRpnFCF4OGHv1n1k=", + "usagesDigest": "PB65rYTG/QbLoSQfRhLDeERG+0m7bjTPzYXBqieQ5/4=", + "recordedFileInputs": { + "@@rules_browsers~//package.json": "84dc1ba9b1c667a25894e97218bd8f247d54f24bb694efb397a881be3c06a4c5" + }, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "angular_cli_npm_typescript": { + "bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl", + "ruleClassName": "http_archive_version", + "attributes": { + "version": "5.9.3", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "urls": [ + "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" + ] + } + }, + "rules_angular_npm_typescript": { + "bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl", + "ruleClassName": "http_archive_version", + "attributes": { + "version": "5.9.2", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "urls": [ + "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" + ] + } + }, + "npm_typescript": { + "bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl", + "ruleClassName": "http_archive_version", + "attributes": { + "version": "5.9.3", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "urls": [ + "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" + ] + } + }, + "npm_rules_browsers_typescript": { + "bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl", + "ruleClassName": "http_archive_version", + "attributes": { + "version": "", + "version_from": "@@rules_browsers~//:package.json", + "integrity": "", + "urls": [ + "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_rules_ts~", + "aspect_rules_ts", + "aspect_rules_ts~" + ], + [ + "aspect_rules_ts~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@aspect_tools_telemetry~//:extension.bzl%telemetry": { + "general": { + "bzlTransitiveDigest": "gA7tPEdJXhskzPIEUxjX9IdDrM6+WjfbgXJ8Ez47umk=", + "usagesDigest": "+m2xFcbJ0y2hkJstxlIUL8Om12UzrH7gt+Fl8h5I/IA=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "aspect_tools_telemetry_report": { + "bzlFile": "@@aspect_tools_telemetry~//:extension.bzl", + "ruleClassName": "tel_repository", + "attributes": { + "deps": { + "aspect_rules_js": "2.8.0", + "aspect_rules_ts": "3.7.1", + "aspect_rules_esbuild": "0.24.0", + "aspect_tools_telemetry": "0.2.8" + } + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_tools_telemetry~", + "aspect_bazel_lib", + "aspect_bazel_lib~" + ], + [ + "aspect_tools_telemetry~", + "bazel_skylib", + "bazel_skylib~" + ] + ] + } + }, + "@@pybind11_bazel~//:python_configure.bzl%extension": { + "general": { + "bzlTransitiveDigest": "dFd3A3f+jPCss+EDKMp/jxjcUhfMku130eT1KGxSCwA=", + "usagesDigest": "gNvOHVcAlwgDsNXD0amkv2CC96mnaCThPQoE44y8K+w=", + "recordedFileInputs": { + "@@pybind11_bazel~//MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e" + }, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_python": { + "bzlFile": "@@pybind11_bazel~//:python_configure.bzl", + "ruleClassName": "python_configure", "attributes": {} }, - "local_config_apple_cc": { - "bzlFile": "@@apple_support~//crosstool:setup.bzl", - "ruleClassName": "_apple_cc_autoconf", + "pybind11": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file": "@@pybind11_bazel~//:pybind11.BUILD", + "strip_prefix": "pybind11-2.11.1", + "urls": [ + "https://github.com/pybind/pybind11/archive/v2.11.1.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "pybind11_bazel~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_angular~//setup:extensions.bzl%rules_angular": { + "general": { + "bzlTransitiveDigest": "fkaH7HMicL3g7/NDaFzlq39kcLopMyQ3KdbDn+5CRzA=", + "usagesDigest": "mthsJSuRvcThgmaeFEDgFmVR6HwM1CSMSOyLlJaCSI0=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "components_rules_angular_configurable_deps": { + "bzlFile": "@@rules_angular~//setup:repositories.bzl", + "ruleClassName": "configurable_deps_repo", + "attributes": { + "angular_compiler_cli": "@@rules_angular~//:node_modules/@angular/compiler-cli", + "typescript": "@@rules_angular~//:node_modules/typescript" + } + }, + "rules_angular_configurable_deps": { + "bzlFile": "@@rules_angular~//setup:repositories.bzl", + "ruleClassName": "configurable_deps_repo", + "attributes": { + "angular_compiler_cli": "@@rules_angular~//:node_modules/@angular/compiler-cli", + "typescript": "@@rules_angular~//:node_modules/typescript-local" + } + }, + "dev_infra_rules_angular_configurable_deps": { + "bzlFile": "@@rules_angular~//setup:repositories.bzl", + "ruleClassName": "configurable_deps_repo", + "attributes": { + "angular_compiler_cli": "@@rules_angular~//:node_modules/@angular/compiler-cli", + "typescript": "@@rules_angular~//:node_modules/typescript" + } + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@rules_browsers~//browsers:extensions.bzl%browsers": { + "general": { + "bzlTransitiveDigest": "6QMCx97Hwh2hyQPqZEA9AKAxbpygF41+K8xJfeqJYm8=", + "usagesDigest": "1PlExi+b77pSr2tAxFCVbpCtFoA7oixHabaL3dmas4Y=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "rules_browsers_chrome_linux": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "4bc6d611d55dc96b213c8605cb8ac27d3c21973bf8b663df4cbf756c989e6745", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/linux64/chrome-headless-shell-linux64.zip" + ], + "named_files": { + "CHROME-HEADLESS-SHELL": "chrome-headless-shell-linux64/chrome-headless-shell" + }, + "exclude_patterns": [ + "**/*.log" + ], + "exports_files": [ + "chrome-headless-shell-linux64/chrome-headless-shell" + ] + } + }, + "rules_browsers_chrome_mac": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "830cc2aafedbe7c9fe671c9898046f8900c06da89d12653ddc3ef26084d2f516", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/mac-x64/chrome-headless-shell-mac-x64.zip" + ], + "named_files": { + "CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-x64/chrome-headless-shell" + }, + "exclude_patterns": [ + "**/*.log" + ], + "exports_files": [ + "chrome-headless-shell-mac-x64/chrome-headless-shell" + ] + } + }, + "rules_browsers_chrome_mac_arm": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "5b5792f5c2d05c3f1f782346910869b61a37b9003f212315b19f4e46710cf8b9", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/mac-arm64/chrome-headless-shell-mac-arm64.zip" + ], + "named_files": { + "CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-arm64/chrome-headless-shell" + }, + "exclude_patterns": [ + "**/*.log" + ], + "exports_files": [ + "chrome-headless-shell-mac-arm64/chrome-headless-shell" + ] + } + }, + "rules_browsers_chrome_win64": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "19bdbf6e1579b6c056b74709520ac9df573f4e80e4f026cc2360a29443cf6c0c", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/win64/chrome-headless-shell-win64.zip" + ], + "named_files": { + "CHROME-HEADLESS-SHELL": "chrome-headless-shell-win64/chrome-headless-shell.exe" + }, + "exclude_patterns": [ + "**/*.log" + ], + "exports_files": [ + "chrome-headless-shell-win64/chrome-headless-shell.exe" + ] + } + }, + "rules_browsers_chromedriver_linux": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "ea41e7a217d878c00e9d66a0724ff54be7d02d08adb7f6458b7d8487b6fbcd84", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/linux64/chromedriver-linux64.zip" + ], + "named_files": { + "CHROMEDRIVER": "chromedriver-linux64/chromedriver" + }, + "exclude_patterns": [], + "exports_files": [ + "chromedriver-linux64/chromedriver" + ] + } + }, + "rules_browsers_chromedriver_mac": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "aede9b67301b930ff9c673df28429aa82ce05c105a4ccbef7e0cd30a97ae429d", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/mac-x64/chromedriver-mac-x64.zip" + ], + "named_files": { + "CHROMEDRIVER": "chromedriver-mac-x64/chromedriver" + }, + "exclude_patterns": [], + "exports_files": [ + "chromedriver-mac-x64/chromedriver" + ] + } + }, + "rules_browsers_chromedriver_mac_arm": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "5adf89a3e8edc6755920f4cfe2fe0515d40684878ef5201da5e02a9d491c4003", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/mac-arm64/chromedriver-mac-arm64.zip" + ], + "named_files": { + "CHROMEDRIVER": "chromedriver-mac-arm64/chromedriver" + }, + "exclude_patterns": [], + "exports_files": [ + "chromedriver-mac-arm64/chromedriver" + ] + } + }, + "rules_browsers_chromedriver_win64": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "a3dfe62b3e9e7a42bd324c07dcbbcc3a733a736b2a59f0e93b9250b88103ab73", + "urls": [ + "https://storage.googleapis.com/chrome-for-testing-public/143.0.7482.0/win64/chromedriver-win64.zip" + ], + "named_files": { + "CHROMEDRIVER": "chromedriver-win64/chromedriver.exe" + }, + "exclude_patterns": [], + "exports_files": [ + "chromedriver-win64/chromedriver.exe" + ] + } + }, + "rules_browsers_firefox_linux": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "c66a48222ff67d51560240d321895c6926c9b3af345cbf688ced8517781d88d1", + "urls": [ + "https://archive.mozilla.org/pub/firefox/releases/144.0/linux-x86_64/en-US/firefox-144.0.tar.xz" + ], + "named_files": { + "FIREFOX": "firefox/firefox" + }, + "exclude_patterns": [], + "exports_files": [ + "firefox/firefox" + ] + } + }, + "rules_browsers_firefox_mac": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "1e444b80921bc999d56c05a7decc1eaf88c0297cac5b90416299af2c77f5ecc9", + "urls": [ + "https://archive.mozilla.org/pub/firefox/releases/144.0/mac/en-US/Firefox%20144.0.dmg" + ], + "named_files": { + "FIREFOX": "Firefox.app/Contents/MacOS/firefox" + }, + "exclude_patterns": [], + "exports_files": [ + "Firefox.app/Contents/MacOS/firefox" + ] + } + }, + "rules_browsers_firefox_mac_arm": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "1e444b80921bc999d56c05a7decc1eaf88c0297cac5b90416299af2c77f5ecc9", + "urls": [ + "https://archive.mozilla.org/pub/firefox/releases/144.0/mac/en-US/Firefox%20144.0.dmg" + ], + "named_files": { + "FIREFOX": "Firefox.app/Contents/MacOS/firefox" + }, + "exclude_patterns": [], + "exports_files": [ + "Firefox.app/Contents/MacOS/firefox" + ] + } + }, + "rules_browsers_firefox_win64": { + "bzlFile": "@@rules_browsers~//browsers/private:browser_repo.bzl", + "ruleClassName": "browser_repo", + "attributes": { + "sha256": "d1e8a7c061e25a41c8dfa85e3aee8e86e9263c69104d80906c978c8d0556563a", + "urls": [ + "https://archive.mozilla.org/pub/firefox/releases/144.0/win64/en-US/Firefox%20Setup%20144.0.exe" + ], + "named_files": { + "FIREFOX": "core/firefox.exe" + }, + "exclude_patterns": [], + "exports_files": [ + "core/firefox.exe" + ] + } + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@rules_fuzzing~//fuzzing/private:extensions.bzl%non_module_dependencies": { + "general": { + "bzlTransitiveDigest": "VMhyxXtdJvrNlLts7afAymA+pOatXuh5kLdxzVAZ/04=", + "usagesDigest": "YnIrdgwnf3iCLfChsltBdZ7yOJh706lpa2vww/i2pDI=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "platforms": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz", + "https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz" + ], + "sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74" + } + }, + "rules_python": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8", + "strip_prefix": "rules_python-0.28.0", + "url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz" + } + }, + "bazel_skylib": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94", + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" + ] + } + }, + "com_google_absl": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "urls": [ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip" + ], + "strip_prefix": "abseil-cpp-20240116.1", + "integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk=" + } + }, + "rules_fuzzing_oss_fuzz": { + "bzlFile": "@@rules_fuzzing~//fuzzing/private/oss_fuzz:repository.bzl", + "ruleClassName": "oss_fuzz_repository", "attributes": {} + }, + "honggfuzz": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "build_file": "@@rules_fuzzing~//:honggfuzz.BUILD", + "sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e", + "url": "https://github.com/google/honggfuzz/archive/2.5.zip", + "strip_prefix": "honggfuzz-2.5" + } + }, + "rules_fuzzing_jazzer": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_jar", + "attributes": { + "sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar" + } + }, + "rules_fuzzing_jazzer_api": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_jar", + "attributes": { + "sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar" + } } }, "recordedRepoMappingEntries": [ [ - "apple_support~", + "rules_fuzzing~", "bazel_tools", "bazel_tools" ] ] } }, - "@@platforms//host:extension.bzl%host_platform": { + "@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": { "general": { - "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=", - "usagesDigest": "pCYpDQmqMbmiiPI1p2Kd3VLm5T48rRAht5WdW0X2GlA=", + "bzlTransitiveDigest": "C4xqrMy1wN4iuTN6Z2eCm94S5XingHhD6uwrIXvCxVI=", + "usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, "generatedRepoSpecs": { - "host_platform": { - "bzlFile": "@@platforms//host:extension.bzl", - "ruleClassName": "host_platform_repo", + "compatibility_proxy": { + "bzlFile": "@@rules_java~//java:rules_java_deps.bzl", + "ruleClassName": "_compatibility_proxy_repo_rule", "attributes": {} } }, + "recordedRepoMappingEntries": [ + [ + "rules_java~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "general": { + "bzlTransitiveDigest": "eecmTsmdIQveoA97hPtH3/Ej/kugbdCI24bhXIXaly8=", + "usagesDigest": "aJF6fLy82rR95Ff5CZPAqxNoFgOMLMN5ImfBS0nhnkg=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "com_github_jetbrains_kotlin_git": { + "bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl", + "ruleClassName": "kotlin_compiler_git_repository", + "attributes": { + "urls": [ + "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" + ], + "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" + } + }, + "com_github_jetbrains_kotlin": { + "bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl", + "ruleClassName": "kotlin_capabilities_repository", + "attributes": { + "git_repository_name": "com_github_jetbrains_kotlin_git", + "compiler_version": "1.9.23" + } + }, + "com_github_google_ksp": { + "bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:ksp.bzl", + "ruleClassName": "ksp_compiler_plugin_repository", + "attributes": { + "urls": [ + "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" + ], + "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", + "strip_version": "1.9.23-1.0.20" + } + }, + "com_github_pinterest_ktlint": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", + "urls": [ + "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" + ], + "executable": true + } + }, + "rules_android": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + "strip_prefix": "rules_android-0.1.1", + "urls": [ + "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_kotlin~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_nodejs~//nodejs:extensions.bzl%node": { + "general": { + "bzlTransitiveDigest": "71PwVsMlLx+RWdt1SI9nSqRHX7DX/NstWwr7/XBxEMs=", + "usagesDigest": "88RybJXEODrqz353smguedoFgMg9se+FF59/WpviJM8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "nodejs_linux_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_amd64" + } + }, + "nodejs_linux_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_arm64" + } + }, + "nodejs_linux_s390x": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_s390x" + } + }, + "nodejs_linux_ppc64le": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_ppc64le" + } + }, + "nodejs_darwin_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "darwin_amd64" + } + }, + "nodejs_darwin_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "darwin_arm64" + } + }, + "nodejs_windows_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "windows_amd64" + } + }, + "nodejs_windows_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "windows_arm64" + } + }, + "nodejs": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "nodejs_host": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "nodejs_toolchains": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_toolchains_repo.bzl", + "ruleClassName": "nodejs_toolchains_repo", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "node20_linux_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "linux_amd64" + } + }, + "node20_linux_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "linux_arm64" + } + }, + "node20_linux_s390x": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "linux_s390x" + } + }, + "node20_linux_ppc64le": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "linux_ppc64le" + } + }, + "node20_darwin_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "darwin_amd64" + } + }, + "node20_darwin_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "darwin_arm64" + } + }, + "node20_windows_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "windows_amd64" + } + }, + "node20_windows_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.0", + "include_headers": false, + "platform": "windows_arm64" + } + }, + "node20": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "node20" + } + }, + "node20_host": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "node20" + } + }, + "node20_toolchains": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_toolchains_repo.bzl", + "ruleClassName": "nodejs_toolchains_repo", + "attributes": { + "user_node_repository_name": "node20" + } + }, + "node22_linux_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "linux_amd64" + } + }, + "node22_linux_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "linux_arm64" + } + }, + "node22_linux_s390x": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "linux_s390x" + } + }, + "node22_linux_ppc64le": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "linux_ppc64le" + } + }, + "node22_darwin_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "darwin_amd64" + } + }, + "node22_darwin_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "darwin_arm64" + } + }, + "node22_windows_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "windows_amd64" + } + }, + "node22_windows_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.12.0", + "include_headers": false, + "platform": "windows_arm64" + } + }, + "node22": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "node22" + } + }, + "node22_host": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "node22" + } + }, + "node22_toolchains": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_toolchains_repo.bzl", + "ruleClassName": "nodejs_toolchains_repo", + "attributes": { + "user_node_repository_name": "node22" + } + }, + "node24_linux_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_amd64" + } + }, + "node24_linux_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_arm64" + } + }, + "node24_linux_s390x": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_s390x" + } + }, + "node24_linux_ppc64le": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "linux_ppc64le" + } + }, + "node24_darwin_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "darwin_amd64" + } + }, + "node24_darwin_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "darwin_arm64" + } + }, + "node24_windows_amd64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "windows_amd64" + } + }, + "node24_windows_arm64": { + "bzlFile": "@@rules_nodejs~//nodejs:repositories.bzl", + "ruleClassName": "_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "24.0.0", + "include_headers": false, + "platform": "windows_arm64" + } + }, + "node24": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "node24" + } + }, + "node24_host": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_repo_host_os_alias.bzl", + "ruleClassName": "nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "node24" + } + }, + "node24_toolchains": { + "bzlFile": "@@rules_nodejs~//nodejs/private:nodejs_toolchains_repo.bzl", + "ruleClassName": "nodejs_toolchains_repo", + "attributes": { + "user_node_repository_name": "node24" + } + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@rules_python~//python/extensions:pip.bzl%pip": { + "general": { + "bzlTransitiveDigest": "UP3OtPScohkFlJevXYy5MF6NWqjUwRxO7SflJmjbM+o=", + "usagesDigest": "K3E4RGDnEgGXkrLOS8/ma4NTUiLvGkMrRIhPiFxv8u0=", + "recordedFileInputs": { + "@@rules_python~//tools/publish/requirements_linux.txt": "8175b4c8df50ae2f22d1706961884beeb54e7da27bd2447018314a175981997d", + "@@rules_fuzzing~//fuzzing/requirements.txt": "ab04664be026b632a0d2a2446c4f65982b7654f5b6851d2f9d399a19b7242a5b", + "@@rules_python~//tools/publish/requirements_windows.txt": "7673adc71dc1a81d3661b90924d7a7c0fc998cd508b3cb4174337cef3f2de556", + "@@protobuf~//python/requirements.txt": "983be60d3cec4b319dcab6d48aeb3f5b2f7c3350f26b3a9e97486c37967c73c5", + "@@rules_python~//tools/publish/requirements_darwin.txt": "2994136eab7e57b083c3de76faf46f70fad130bc8e7360a7fed2b288b69e79dc" + }, + "recordedDirentsInputs": {}, + "envVariables": { + "RULES_PYTHON_REPO_DEBUG": null, + "RULES_PYTHON_REPO_DEBUG_VERBOSITY": null + }, + "generatedRepoSpecs": { + "pip_deps_310_numpy": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_10_host//:python", + "repo": "pip_deps_310", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_310_setuptools": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_10_host//:python", + "repo": "pip_deps_310", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_311_numpy": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "pip_deps_311", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_311_setuptools": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "pip_deps_311", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_312_numpy": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_12_host//:python", + "repo": "pip_deps_312", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_312_setuptools": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_12_host//:python", + "repo": "pip_deps_312", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_38_numpy": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_8_host//:python", + "repo": "pip_deps_38", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_38_setuptools": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_8_host//:python", + "repo": "pip_deps_38", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_39_numpy": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_9_host//:python", + "repo": "pip_deps_39", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_39_setuptools": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python~~python~python_3_9_host//:python", + "repo": "pip_deps_39", + "requirement": "setuptools<=70.3.0" + } + }, + "rules_fuzzing_py_deps_310_absl_py": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_10_host//:python", + "repo": "rules_fuzzing_py_deps_310", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_310_six": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_10_host//:python", + "repo": "rules_fuzzing_py_deps_310", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_311_absl_py": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_fuzzing_py_deps_311", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_311_six": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_fuzzing_py_deps_311", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_312_absl_py": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_12_host//:python", + "repo": "rules_fuzzing_py_deps_312", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_312_six": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_12_host//:python", + "repo": "rules_fuzzing_py_deps_312", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_38_absl_py": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_8_host//:python", + "repo": "rules_fuzzing_py_deps_38", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_38_six": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_8_host//:python", + "repo": "rules_fuzzing_py_deps_38", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_39_absl_py": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_9_host//:python", + "repo": "rules_fuzzing_py_deps_39", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_39_six": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python~~python~python_3_9_host//:python", + "repo": "rules_fuzzing_py_deps_39", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_python_publish_deps_311_backports_tarfile_py3_none_any_77e284d7": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "backports.tarfile-1.2.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "backports-tarfile==1.2.0", + "sha256": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "urls": [ + "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_backports_tarfile_sdist_d75e02c2": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "backports_tarfile-1.2.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "backports-tarfile==1.2.0", + "sha256": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", + "urls": [ + "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_certifi_py3_none_any_922820b5": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "certifi-2024.8.30-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "certifi==2024.8.30", + "sha256": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "urls": [ + "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_certifi_sdist_bec941d2": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "certifi-2024.8.30.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "certifi==2024.8.30", + "sha256": "bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", + "urls": [ + "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_a1ed2dd2": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "urls": [ + "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_ppc64le_46bf4316": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "urls": [ + "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_s390x_a24ed04c": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "urls": [ + "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_610faea7": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "urls": [ + "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_aarch64_a9b15d49": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "urls": [ + "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_fc48c783": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", + "urls": [ + "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_sdist_1c39c601": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "cffi-1.17.1.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "urls": [ + "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_universal2_0d99dd8f": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "urls": [ + "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_x86_64_c57516e5": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "urls": [ + "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_11_0_arm64_6dba5d19": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "urls": [ + "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_aarch64_bf4475b8": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "urls": [ + "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_ppc64le_ce031db0": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "urls": [ + "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_s390x_8ff4e7cd": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "urls": [ + "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_3710a975": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "urls": [ + "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_aarch64_47334db7": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "urls": [ + "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_ppc64le_f1a2f519": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "urls": [ + "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_s390x_63bc5c4a": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "urls": [ + "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_x86_64_bcb4f8ea": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "urls": [ + "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_cee4373f": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "urls": [ + "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_py3_none_any_fe9f97fe": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "urls": [ + "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_sdist_223217c3": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "charset_normalizer-3.4.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "urls": [ + "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_aarch64_846da004": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "urls": [ + "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_x86_64_0f996e72": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "urls": [ + "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_aarch64_f7b178f1": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", + "urls": [ + "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_x86_64_c2e6fc39": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "urls": [ + "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_e1be4655": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "urls": [ + "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_df6b6c6d": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "urls": [ + "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_sdist_315b9001": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "cryptography-43.0.3.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "urls": [ + "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "docutils-0.21.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "docutils==0.21.2", + "sha256": "dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", + "urls": [ + "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_docutils_sdist_3a6b1873": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "docutils-0.21.2.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "docutils==0.21.2", + "sha256": "3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "urls": [ + "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_idna_py3_none_any_946d195a": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "idna-3.10-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "idna==3.10", + "sha256": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", + "urls": [ + "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_idna_sdist_12f65c9b": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "idna-3.10.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "idna==3.10", + "sha256": "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "urls": [ + "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_importlib_metadata_py3_none_any_45e54197": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "importlib_metadata-8.5.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "importlib-metadata==8.5.0", + "sha256": "45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", + "urls": [ + "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_importlib_metadata_sdist_71522656": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "importlib_metadata-8.5.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "importlib-metadata==8.5.0", + "sha256": "71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", + "urls": [ + "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_classes_py3_none_any_f662826b": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.classes-3.4.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-classes==3.4.0", + "sha256": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", + "urls": [ + "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_classes_sdist_47a024b5": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco.classes-3.4.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-classes==3.4.0", + "sha256": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "urls": [ + "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_context_py3_none_any_f797fc48": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.context-6.0.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-context==6.0.1", + "sha256": "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", + "urls": [ + "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_context_sdist_9bae4ea5": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco_context-6.0.1.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-context==6.0.1", + "sha256": "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "urls": [ + "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_functools_py3_none_any_ad159f13": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.functools-4.1.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-functools==4.1.0", + "sha256": "ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", + "urls": [ + "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_functools_sdist_70f7e0e2": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco_functools-4.1.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-functools==4.1.0", + "sha256": "70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", + "urls": [ + "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "jeepney-0.8.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jeepney==0.8.0", + "sha256": "c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", + "urls": [ + "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jeepney_sdist_5efe48d2": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jeepney-0.8.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jeepney==0.8.0", + "sha256": "5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "urls": [ + "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_keyring_py3_none_any_5426f817": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "keyring-25.4.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "keyring==25.4.1", + "sha256": "5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf", + "urls": [ + "https://files.pythonhosted.org/packages/83/25/e6d59e5f0a0508d0dca8bb98c7f7fd3772fc943ac3f53d5ab18a218d32c0/keyring-25.4.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_keyring_sdist_b07ebc55": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "keyring-25.4.1.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "keyring==25.4.1", + "sha256": "b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b", + "urls": [ + "https://files.pythonhosted.org/packages/a5/1c/2bdbcfd5d59dc6274ffb175bc29aa07ecbfab196830e0cfbde7bd861a2ea/keyring-25.4.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_markdown_it_py_py3_none_any_35521684": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "markdown_it_py-3.0.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "markdown-it-py==3.0.0", + "sha256": "355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "urls": [ + "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_markdown_it_py_sdist_e3f60a94": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "markdown-it-py-3.0.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "markdown-it-py==3.0.0", + "sha256": "e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", + "urls": [ + "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_mdurl_py3_none_any_84008a41": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "mdurl-0.1.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "mdurl==0.1.2", + "sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "urls": [ + "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_mdurl_sdist_bb413d29": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "mdurl-0.1.2.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "mdurl==0.1.2", + "sha256": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", + "urls": [ + "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_more_itertools_py3_none_any_037b0d32": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "more_itertools-10.5.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "more-itertools==10.5.0", + "sha256": "037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "urls": [ + "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_more_itertools_sdist_5482bfef": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "more-itertools-10.5.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "more-itertools==10.5.0", + "sha256": "5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", + "urls": [ + "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_14c5a72e": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", + "urls": [ + "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_7b7c2a3c": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", + "urls": [ + "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_aarch64_42c64511": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", + "urls": [ + "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_armv7l_0411beb0": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", + "urls": [ + "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64_5f36b271": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", + "urls": [ + "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64le_34c03fa7": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", + "urls": [ + "https://files.pythonhosted.org/packages/ab/a7/375afcc710dbe2d64cfbd69e31f82f3e423d43737258af01f6a56d844085/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_s390x_19aaba96": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", + "urls": [ + "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_x86_64_de3ceed6": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", + "urls": [ + "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_aarch64_f0eca9ca": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", + "urls": [ + "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_armv7l_3a157ab1": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", + "urls": [ + "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_x86_64_36c95d4b": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", + "urls": [ + "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_win_amd64_8ce0f819": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-win_amd64.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", + "urls": [ + "https://files.pythonhosted.org/packages/26/8d/53c5b19c4999bdc6ba95f246f4ef35ca83d7d7423e5e38be43ad66544e5d/nh3-0.2.18-cp37-abi3-win_amd64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_sdist_94a16692": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "nh3-0.2.18.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", + "urls": [ + "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pkginfo_py3_none_any_889a6da2": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "pkginfo-1.10.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pkginfo==1.10.0", + "sha256": "889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", + "urls": [ + "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pkginfo_sdist_5df73835": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pkginfo-1.10.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pkginfo==1.10.0", + "sha256": "5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", + "urls": [ + "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pycparser_py3_none_any_c3702b6d": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "pycparser-2.22-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pycparser==2.22", + "sha256": "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", + "urls": [ + "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pycparser_sdist_491c8be9": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pycparser-2.22.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pycparser==2.22", + "sha256": "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "urls": [ + "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pygments_py3_none_any_b8e6aca0": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "pygments-2.18.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pygments==2.18.0", + "sha256": "b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", + "urls": [ + "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pygments_sdist_786ff802": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pygments-2.18.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pygments==2.18.0", + "sha256": "786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "urls": [ + "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pywin32_ctypes_py3_none_any_8a151337": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_windows_x86_64" + ], + "filename": "pywin32_ctypes-0.2.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pywin32-ctypes==0.2.3", + "sha256": "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "urls": [ + "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pywin32_ctypes_sdist_d162dc04": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pywin32-ctypes-0.2.3.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pywin32-ctypes==0.2.3", + "sha256": "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", + "urls": [ + "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_readme_renderer_py3_none_any_2fbca89b": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "readme_renderer-44.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "readme-renderer==44.0", + "sha256": "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "urls": [ + "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_readme_renderer_sdist_8712034e": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "readme_renderer-44.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "readme-renderer==44.0", + "sha256": "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", + "urls": [ + "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_requests_py3_none_any_70761cfe": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "requests-2.32.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests==2.32.3", + "sha256": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", + "urls": [ + "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_requests_sdist_55365417": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "requests-2.32.3.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests==2.32.3", + "sha256": "55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "urls": [ + "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_requests_toolbelt_py2_none_any_cccfdd66": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "requests_toolbelt-1.0.0-py2.py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests-toolbelt==1.0.0", + "sha256": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", + "urls": [ + "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_requests_toolbelt_sdist_7681a0a3": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "requests-toolbelt-1.0.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests-toolbelt==1.0.0", + "sha256": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "urls": [ + "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "rfc3986-2.0.0-py2.py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rfc3986==2.0.0", + "sha256": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "urls": [ + "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_rfc3986_sdist_97aacf9d": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "rfc3986-2.0.0.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rfc3986==2.0.0", + "sha256": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", + "urls": [ + "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_rich_py3_none_any_9836f509": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "rich-13.9.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rich==13.9.3", + "sha256": "9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", + "urls": [ + "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_rich_sdist_bc1e01b8": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "rich-13.9.3.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rich==13.9.3", + "sha256": "bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", + "urls": [ + "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_secretstorage_py3_none_any_f356e662": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "SecretStorage-3.3.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "secretstorage==3.3.3", + "sha256": "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", + "urls": [ + "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_secretstorage_sdist_2403533e": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "SecretStorage-3.3.3.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "secretstorage==3.3.3", + "sha256": "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "urls": [ + "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_twine_py3_none_any_215dbe7b": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "twine-5.1.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "twine==5.1.1", + "sha256": "215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", + "urls": [ + "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_twine_sdist_9aa08251": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "twine-5.1.1.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "twine==5.1.1", + "sha256": "9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", + "urls": [ + "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_urllib3_py3_none_any_ca899ca0": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "urllib3-2.2.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "urllib3==2.2.3", + "sha256": "ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "urls": [ + "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_urllib3_sdist_e7d814a8": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "urllib3-2.2.3.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "urllib3==2.2.3", + "sha256": "e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", + "urls": [ + "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_zipp_py3_none_any_a817ac80": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "zipp-3.20.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "zipp==3.20.2", + "sha256": "a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "urls": [ + "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_zipp_sdist_bc9eb26f": { + "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl", + "ruleClassName": "whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "zipp-3.20.2.tar.gz", + "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "zipp==3.20.2", + "sha256": "bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", + "urls": [ + "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz" + ] + } + }, + "pip_deps": { + "bzlFile": "@@rules_python~//python/private/pypi:hub_repository.bzl", + "ruleClassName": "hub_repository", + "attributes": { + "repo_name": "pip_deps", + "extra_hub_aliases": {}, + "whl_map": { + "numpy": "{\"pip_deps_310_numpy\":[{\"version\":\"3.10\"}],\"pip_deps_311_numpy\":[{\"version\":\"3.11\"}],\"pip_deps_312_numpy\":[{\"version\":\"3.12\"}],\"pip_deps_38_numpy\":[{\"version\":\"3.8\"}],\"pip_deps_39_numpy\":[{\"version\":\"3.9\"}]}", + "setuptools": "{\"pip_deps_310_setuptools\":[{\"version\":\"3.10\"}],\"pip_deps_311_setuptools\":[{\"version\":\"3.11\"}],\"pip_deps_312_setuptools\":[{\"version\":\"3.12\"}],\"pip_deps_38_setuptools\":[{\"version\":\"3.8\"}],\"pip_deps_39_setuptools\":[{\"version\":\"3.9\"}]}" + }, + "packages": [ + "numpy", + "setuptools" + ], + "groups": {} + } + }, + "rules_fuzzing_py_deps": { + "bzlFile": "@@rules_python~//python/private/pypi:hub_repository.bzl", + "ruleClassName": "hub_repository", + "attributes": { + "repo_name": "rules_fuzzing_py_deps", + "extra_hub_aliases": {}, + "whl_map": { + "absl_py": "{\"rules_fuzzing_py_deps_310_absl_py\":[{\"version\":\"3.10\"}],\"rules_fuzzing_py_deps_311_absl_py\":[{\"version\":\"3.11\"}],\"rules_fuzzing_py_deps_312_absl_py\":[{\"version\":\"3.12\"}],\"rules_fuzzing_py_deps_38_absl_py\":[{\"version\":\"3.8\"}],\"rules_fuzzing_py_deps_39_absl_py\":[{\"version\":\"3.9\"}]}", + "six": "{\"rules_fuzzing_py_deps_310_six\":[{\"version\":\"3.10\"}],\"rules_fuzzing_py_deps_311_six\":[{\"version\":\"3.11\"}],\"rules_fuzzing_py_deps_312_six\":[{\"version\":\"3.12\"}],\"rules_fuzzing_py_deps_38_six\":[{\"version\":\"3.8\"}],\"rules_fuzzing_py_deps_39_six\":[{\"version\":\"3.9\"}]}" + }, + "packages": [ + "absl_py", + "six" + ], + "groups": {} + } + }, + "rules_python_publish_deps": { + "bzlFile": "@@rules_python~//python/private/pypi:hub_repository.bzl", + "ruleClassName": "hub_repository", + "attributes": { + "repo_name": "rules_python_publish_deps", + "extra_hub_aliases": {}, + "whl_map": { + "backports_tarfile": "{\"rules_python_publish_deps_311_backports_tarfile_py3_none_any_77e284d7\":[{\"filename\":\"backports.tarfile-1.2.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_backports_tarfile_sdist_d75e02c2\":[{\"filename\":\"backports_tarfile-1.2.0.tar.gz\",\"version\":\"3.11\"}]}", + "certifi": "{\"rules_python_publish_deps_311_certifi_py3_none_any_922820b5\":[{\"filename\":\"certifi-2024.8.30-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_certifi_sdist_bec941d2\":[{\"filename\":\"certifi-2024.8.30.tar.gz\",\"version\":\"3.11\"}]}", + "cffi": "{\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_a1ed2dd2\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_ppc64le_46bf4316\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_s390x_a24ed04c\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_610faea7\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_aarch64_a9b15d49\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_fc48c783\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_sdist_1c39c601\":[{\"filename\":\"cffi-1.17.1.tar.gz\",\"version\":\"3.11\"}]}", + "charset_normalizer": "{\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_universal2_0d99dd8f\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_x86_64_c57516e5\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_11_0_arm64_6dba5d19\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_aarch64_bf4475b8\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_ppc64le_ce031db0\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_s390x_8ff4e7cd\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_3710a975\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_aarch64_47334db7\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_ppc64le_f1a2f519\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_s390x_63bc5c4a\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_x86_64_bcb4f8ea\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_cee4373f\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_py3_none_any_fe9f97fe\":[{\"filename\":\"charset_normalizer-3.4.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_sdist_223217c3\":[{\"filename\":\"charset_normalizer-3.4.0.tar.gz\",\"version\":\"3.11\"}]}", + "cryptography": "{\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_aarch64_846da004\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_x86_64_0f996e72\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_aarch64_f7b178f1\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_x86_64_c2e6fc39\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_e1be4655\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_df6b6c6d\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_sdist_315b9001\":[{\"filename\":\"cryptography-43.0.3.tar.gz\",\"version\":\"3.11\"}]}", + "docutils": "{\"rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9\":[{\"filename\":\"docutils-0.21.2-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_docutils_sdist_3a6b1873\":[{\"filename\":\"docutils-0.21.2.tar.gz\",\"version\":\"3.11\"}]}", + "idna": "{\"rules_python_publish_deps_311_idna_py3_none_any_946d195a\":[{\"filename\":\"idna-3.10-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_idna_sdist_12f65c9b\":[{\"filename\":\"idna-3.10.tar.gz\",\"version\":\"3.11\"}]}", + "importlib_metadata": "{\"rules_python_publish_deps_311_importlib_metadata_py3_none_any_45e54197\":[{\"filename\":\"importlib_metadata-8.5.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_importlib_metadata_sdist_71522656\":[{\"filename\":\"importlib_metadata-8.5.0.tar.gz\",\"version\":\"3.11\"}]}", + "jaraco_classes": "{\"rules_python_publish_deps_311_jaraco_classes_py3_none_any_f662826b\":[{\"filename\":\"jaraco.classes-3.4.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jaraco_classes_sdist_47a024b5\":[{\"filename\":\"jaraco.classes-3.4.0.tar.gz\",\"version\":\"3.11\"}]}", + "jaraco_context": "{\"rules_python_publish_deps_311_jaraco_context_py3_none_any_f797fc48\":[{\"filename\":\"jaraco.context-6.0.1-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jaraco_context_sdist_9bae4ea5\":[{\"filename\":\"jaraco_context-6.0.1.tar.gz\",\"version\":\"3.11\"}]}", + "jaraco_functools": "{\"rules_python_publish_deps_311_jaraco_functools_py3_none_any_ad159f13\":[{\"filename\":\"jaraco.functools-4.1.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jaraco_functools_sdist_70f7e0e2\":[{\"filename\":\"jaraco_functools-4.1.0.tar.gz\",\"version\":\"3.11\"}]}", + "jeepney": "{\"rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad\":[{\"filename\":\"jeepney-0.8.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jeepney_sdist_5efe48d2\":[{\"filename\":\"jeepney-0.8.0.tar.gz\",\"version\":\"3.11\"}]}", + "keyring": "{\"rules_python_publish_deps_311_keyring_py3_none_any_5426f817\":[{\"filename\":\"keyring-25.4.1-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_keyring_sdist_b07ebc55\":[{\"filename\":\"keyring-25.4.1.tar.gz\",\"version\":\"3.11\"}]}", + "markdown_it_py": "{\"rules_python_publish_deps_311_markdown_it_py_py3_none_any_35521684\":[{\"filename\":\"markdown_it_py-3.0.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_markdown_it_py_sdist_e3f60a94\":[{\"filename\":\"markdown-it-py-3.0.0.tar.gz\",\"version\":\"3.11\"}]}", + "mdurl": "{\"rules_python_publish_deps_311_mdurl_py3_none_any_84008a41\":[{\"filename\":\"mdurl-0.1.2-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_mdurl_sdist_bb413d29\":[{\"filename\":\"mdurl-0.1.2.tar.gz\",\"version\":\"3.11\"}]}", + "more_itertools": "{\"rules_python_publish_deps_311_more_itertools_py3_none_any_037b0d32\":[{\"filename\":\"more_itertools-10.5.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_more_itertools_sdist_5482bfef\":[{\"filename\":\"more-itertools-10.5.0.tar.gz\",\"version\":\"3.11\"}]}", + "nh3": "{\"rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_14c5a72e\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_7b7c2a3c\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_aarch64_42c64511\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_armv7l_0411beb0\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64_5f36b271\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64le_34c03fa7\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_s390x_19aaba96\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_x86_64_de3ceed6\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_aarch64_f0eca9ca\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_armv7l_3a157ab1\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_x86_64_36c95d4b\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_win_amd64_8ce0f819\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-win_amd64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_sdist_94a16692\":[{\"filename\":\"nh3-0.2.18.tar.gz\",\"version\":\"3.11\"}]}", + "pkginfo": "{\"rules_python_publish_deps_311_pkginfo_py3_none_any_889a6da2\":[{\"filename\":\"pkginfo-1.10.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pkginfo_sdist_5df73835\":[{\"filename\":\"pkginfo-1.10.0.tar.gz\",\"version\":\"3.11\"}]}", + "pycparser": "{\"rules_python_publish_deps_311_pycparser_py3_none_any_c3702b6d\":[{\"filename\":\"pycparser-2.22-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pycparser_sdist_491c8be9\":[{\"filename\":\"pycparser-2.22.tar.gz\",\"version\":\"3.11\"}]}", + "pygments": "{\"rules_python_publish_deps_311_pygments_py3_none_any_b8e6aca0\":[{\"filename\":\"pygments-2.18.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pygments_sdist_786ff802\":[{\"filename\":\"pygments-2.18.0.tar.gz\",\"version\":\"3.11\"}]}", + "pywin32_ctypes": "{\"rules_python_publish_deps_311_pywin32_ctypes_py3_none_any_8a151337\":[{\"filename\":\"pywin32_ctypes-0.2.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pywin32_ctypes_sdist_d162dc04\":[{\"filename\":\"pywin32-ctypes-0.2.3.tar.gz\",\"version\":\"3.11\"}]}", + "readme_renderer": "{\"rules_python_publish_deps_311_readme_renderer_py3_none_any_2fbca89b\":[{\"filename\":\"readme_renderer-44.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_readme_renderer_sdist_8712034e\":[{\"filename\":\"readme_renderer-44.0.tar.gz\",\"version\":\"3.11\"}]}", + "requests": "{\"rules_python_publish_deps_311_requests_py3_none_any_70761cfe\":[{\"filename\":\"requests-2.32.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_requests_sdist_55365417\":[{\"filename\":\"requests-2.32.3.tar.gz\",\"version\":\"3.11\"}]}", + "requests_toolbelt": "{\"rules_python_publish_deps_311_requests_toolbelt_py2_none_any_cccfdd66\":[{\"filename\":\"requests_toolbelt-1.0.0-py2.py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_requests_toolbelt_sdist_7681a0a3\":[{\"filename\":\"requests-toolbelt-1.0.0.tar.gz\",\"version\":\"3.11\"}]}", + "rfc3986": "{\"rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b\":[{\"filename\":\"rfc3986-2.0.0-py2.py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_rfc3986_sdist_97aacf9d\":[{\"filename\":\"rfc3986-2.0.0.tar.gz\",\"version\":\"3.11\"}]}", + "rich": "{\"rules_python_publish_deps_311_rich_py3_none_any_9836f509\":[{\"filename\":\"rich-13.9.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_rich_sdist_bc1e01b8\":[{\"filename\":\"rich-13.9.3.tar.gz\",\"version\":\"3.11\"}]}", + "secretstorage": "{\"rules_python_publish_deps_311_secretstorage_py3_none_any_f356e662\":[{\"filename\":\"SecretStorage-3.3.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_secretstorage_sdist_2403533e\":[{\"filename\":\"SecretStorage-3.3.3.tar.gz\",\"version\":\"3.11\"}]}", + "twine": "{\"rules_python_publish_deps_311_twine_py3_none_any_215dbe7b\":[{\"filename\":\"twine-5.1.1-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_twine_sdist_9aa08251\":[{\"filename\":\"twine-5.1.1.tar.gz\",\"version\":\"3.11\"}]}", + "urllib3": "{\"rules_python_publish_deps_311_urllib3_py3_none_any_ca899ca0\":[{\"filename\":\"urllib3-2.2.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_urllib3_sdist_e7d814a8\":[{\"filename\":\"urllib3-2.2.3.tar.gz\",\"version\":\"3.11\"}]}", + "zipp": "{\"rules_python_publish_deps_311_zipp_py3_none_any_a817ac80\":[{\"filename\":\"zipp-3.20.2-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_zipp_sdist_bc9eb26f\":[{\"filename\":\"zipp-3.20.2.tar.gz\",\"version\":\"3.11\"}]}" + }, + "packages": [ + "backports_tarfile", + "certifi", + "charset_normalizer", + "docutils", + "idna", + "importlib_metadata", + "jaraco_classes", + "jaraco_context", + "jaraco_functools", + "keyring", + "markdown_it_py", + "mdurl", + "more_itertools", + "nh3", + "pkginfo", + "pygments", + "readme_renderer", + "requests", + "requests_toolbelt", + "rfc3986", + "rich", + "twine", + "urllib3", + "zipp" + ], + "groups": {} + } + } + }, + "moduleExtensionMetadata": { + "useAllRepos": "NO", + "reproducible": false + }, + "recordedRepoMappingEntries": [ + [ + "bazel_features~", + "bazel_features_globals", + "bazel_features~~version_extension~bazel_features_globals" + ], + [ + "bazel_features~", + "bazel_features_version", + "bazel_features~~version_extension~bazel_features_version" + ], + [ + "rules_python~", + "bazel_features", + "bazel_features~" + ], + [ + "rules_python~", + "bazel_skylib", + "bazel_skylib~" + ], + [ + "rules_python~", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_python~", + "pypi__build", + "rules_python~~internal_deps~pypi__build" + ], + [ + "rules_python~", + "pypi__click", + "rules_python~~internal_deps~pypi__click" + ], + [ + "rules_python~", + "pypi__colorama", + "rules_python~~internal_deps~pypi__colorama" + ], + [ + "rules_python~", + "pypi__importlib_metadata", + "rules_python~~internal_deps~pypi__importlib_metadata" + ], + [ + "rules_python~", + "pypi__installer", + "rules_python~~internal_deps~pypi__installer" + ], + [ + "rules_python~", + "pypi__more_itertools", + "rules_python~~internal_deps~pypi__more_itertools" + ], + [ + "rules_python~", + "pypi__packaging", + "rules_python~~internal_deps~pypi__packaging" + ], + [ + "rules_python~", + "pypi__pep517", + "rules_python~~internal_deps~pypi__pep517" + ], + [ + "rules_python~", + "pypi__pip", + "rules_python~~internal_deps~pypi__pip" + ], + [ + "rules_python~", + "pypi__pip_tools", + "rules_python~~internal_deps~pypi__pip_tools" + ], + [ + "rules_python~", + "pypi__pyproject_hooks", + "rules_python~~internal_deps~pypi__pyproject_hooks" + ], + [ + "rules_python~", + "pypi__setuptools", + "rules_python~~internal_deps~pypi__setuptools" + ], + [ + "rules_python~", + "pypi__tomli", + "rules_python~~internal_deps~pypi__tomli" + ], + [ + "rules_python~", + "pypi__wheel", + "rules_python~~internal_deps~pypi__wheel" + ], + [ + "rules_python~", + "pypi__zipp", + "rules_python~~internal_deps~pypi__zipp" + ], + [ + "rules_python~", + "pythons_hub", + "rules_python~~python~pythons_hub" + ], + [ + "rules_python~~python~pythons_hub", + "python_3_10_host", + "rules_python~~python~python_3_10_host" + ], + [ + "rules_python~~python~pythons_hub", + "python_3_11_host", + "rules_python~~python~python_3_11_host" + ], + [ + "rules_python~~python~pythons_hub", + "python_3_12_host", + "rules_python~~python~python_3_12_host" + ], + [ + "rules_python~~python~pythons_hub", + "python_3_8_host", + "rules_python~~python~python_3_8_host" + ], + [ + "rules_python~~python~pythons_hub", + "python_3_9_host", + "rules_python~~python~python_3_9_host" + ] + ] + } + }, + "@@rules_sass~//src/toolchain:extensions.bzl%sass": { + "general": { + "bzlTransitiveDigest": "RA58Nyrsn03Z5YmQnpmBw3mqlVck++XIrx34amsqU/E=", + "usagesDigest": "FPXQ5+6+DFGdSdCMXLwFaruzstMFlLH6N0TRxi0sSH8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "linux_amd64_sass": { + "bzlFile": "@@rules_sass~//src/toolchain:configure_sass.bzl", + "ruleClassName": "configure_sass", + "attributes": { + "file": "@rules_sass//src/compiler/built:sass_linux_x64", + "sha256": "", + "constraints": [ + "@@platforms//os:linux", + "@@platforms//cpu:x86_64" + ] + } + }, + "linux_arm64_sass": { + "bzlFile": "@@rules_sass~//src/toolchain:configure_sass.bzl", + "ruleClassName": "configure_sass", + "attributes": { + "file": "@rules_sass//src/compiler/built:sass_linux_arm", + "sha256": "", + "constraints": [ + "@@platforms//os:linux", + "@@platforms//cpu:arm64" + ] + } + }, + "darwin_amd64_sass": { + "bzlFile": "@@rules_sass~//src/toolchain:configure_sass.bzl", + "ruleClassName": "configure_sass", + "attributes": { + "file": "@rules_sass//src/compiler/built:sass_mac_x64", + "sha256": "", + "constraints": [ + "@@platforms//os:macos", + "@@platforms//cpu:x86_64" + ] + } + }, + "darwin_arm64_sass": { + "bzlFile": "@@rules_sass~//src/toolchain:configure_sass.bzl", + "ruleClassName": "configure_sass", + "attributes": { + "file": "@rules_sass//src/compiler/built:sass_mac_arm", + "sha256": "", + "constraints": [ + "@@platforms//os:macos", + "@@platforms//cpu:arm64" + ] + } + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@yq.bzl~//yq:extensions.bzl%yq": { + "general": { + "bzlTransitiveDigest": "61Uz+o5PnlY0jJfPZEUNqsKxnM/UCLeWsn5VVCc8u5Y=", + "usagesDigest": "X+3wxc/+KjF0tyJJ5qca2U/BgoeAEmAD0kuz8iBcX+0=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "yq_darwin_amd64": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "darwin_amd64", + "version": "4.45.1" + } + }, + "yq_darwin_arm64": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "darwin_arm64", + "version": "4.45.1" + } + }, + "yq_linux_amd64": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "linux_amd64", + "version": "4.45.1" + } + }, + "yq_linux_arm64": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "linux_arm64", + "version": "4.45.1" + } + }, + "yq_linux_s390x": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "linux_s390x", + "version": "4.45.1" + } + }, + "yq_linux_riscv64": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "linux_riscv64", + "version": "4.45.1" + } + }, + "yq_linux_ppc64le": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "linux_ppc64le", + "version": "4.45.1" + } + }, + "yq_windows_amd64": { + "bzlFile": "@@yq.bzl~//yq/toolchain:platforms.bzl", + "ruleClassName": "yq_platform_repo", + "attributes": { + "platform": "windows_amd64", + "version": "4.45.1" + } + }, + "yq_toolchains": { + "bzlFile": "@@yq.bzl~//yq/toolchain:toolchain.bzl", + "ruleClassName": "yq_toolchains_repo", + "attributes": { + "user_repository_name": "yq" + } + } + }, "recordedRepoMappingEntries": [] } } diff --git a/WORKSPACE b/WORKSPACE deleted file mode 100644 index bdd6ac8c8c37..000000000000 --- a/WORKSPACE +++ /dev/null @@ -1,294 +0,0 @@ -workspace(name = "angular_cli") - -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") - -http_archive( - name = "bazel_skylib", - sha256 = "51b5105a760b353773f904d2bbc5e664d0987fbaf22265164de65d43e910d8ac", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.8.1/bazel-skylib-1.8.1.tar.gz", - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.8.1/bazel-skylib-1.8.1.tar.gz", - ], -) - -http_archive( - name = "io_bazel_rules_webtesting", - sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a", - urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz"], -) - -http_archive( - name = "aspect_rules_js", - sha256 = "b71565da7a811964e30cccb405544d551561e4b56c65f0c0aeabe85638920bd6", - strip_prefix = "rules_js-2.4.2", - url = "https://github.com/aspect-build/rules_js/releases/download/v2.4.2/rules_js-v2.4.2.tar.gz", -) - -load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies") - -rules_js_dependencies() - -http_archive( - name = "rules_pkg", - sha256 = "8c20f74bca25d2d442b327ae26768c02cf3c99e93fad0381f32be9aab1967675", - urls = ["https://github.com/bazelbuild/rules_pkg/releases/download/0.8.1/rules_pkg-0.8.1.tar.gz"], -) - -load("@bazel_tools//tools/sh:sh_configure.bzl", "sh_configure") - -sh_configure() - -load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") - -bazel_skylib_workspace() - -load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") - -rules_pkg_dependencies() - -# Setup the Node.js toolchain -load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains") - -# Set the default nodejs toolchain to the latest supported major version - -NODE_24_VERSION = "24.0.0" - -NODE_24_REPO = { - "24.0.0-darwin_arm64": ("node-v24.0.0-darwin-arm64.tar.gz", "node-v24.0.0-darwin-arm64", "194e2f3dd3ec8c2adcaa713ed40f44c5ca38467880e160974ceac1659be60121"), - "24.0.0-darwin_amd64": ("node-v24.0.0-darwin-x64.tar.gz", "node-v24.0.0-darwin-x64", "f716b3ce14a7e37a6cbf97c9de10d444d7da07ef833cd8da81dd944d111e6a4a"), - "24.0.0-linux_arm64": ("node-v24.0.0-linux-arm64.tar.xz", "node-v24.0.0-linux-arm64", "d40ec7ffe0b82b02dce94208c84351424099bd70fa3a42b65c46d95322305040"), - "24.0.0-linux_ppc64le": ("node-v24.0.0-linux-ppc64le.tar.xz", "node-v24.0.0-linux-ppc64le", "cfa0e8d51a2f9a446f1bfb81cdf4c7e95336ad622e2aa230e3fa1d093c63d77d"), - "24.0.0-linux_s390x": ("node-v24.0.0-linux-s390x.tar.xz", "node-v24.0.0-linux-s390x", "e37a04c7ee05416ec1234fd3255e05b6b81287eb0424a57441c8b69f0a155021"), - "24.0.0-linux_amd64": ("node-v24.0.0-linux-x64.tar.xz", "node-v24.0.0-linux-x64", "59b8af617dccd7f9f68cc8451b2aee1e86d6bd5cb92cd51dd6216a31b707efd7"), - "24.0.0-windows_amd64": ("node-v24.0.0-win-x64.zip", "node-v24.0.0-win-x64", "3d0fff80c87bb9a8d7f49f2f27832aa34a1477d137af46f5b14df5498be81304"), -} - -nodejs_register_toolchains( - name = "nodejs", - node_repositories = NODE_24_REPO, - node_version = NODE_24_VERSION, -) - -nodejs_register_toolchains( - name = "node20", - node_repositories = { - "20.19.0-darwin_arm64": ("node-v20.19.0-darwin-arm64.tar.gz", "node-v20.19.0-darwin-arm64", "c016cd1975a264a29dc1b07c6fbe60d5df0a0c2beb4113c0450e3d998d1a0d9c"), - "20.19.0-darwin_amd64": ("node-v20.19.0-darwin-x64.tar.gz", "node-v20.19.0-darwin-x64", "a8554af97d6491fdbdabe63d3a1cfb9571228d25a3ad9aed2df856facb131b20"), - "20.19.0-linux_arm64": ("node-v20.19.0-linux-arm64.tar.xz", "node-v20.19.0-linux-arm64", "dbe339e55eb393955a213e6b872066880bb9feceaa494f4d44c7aac205ec2ab9"), - "20.19.0-linux_ppc64le": ("node-v20.19.0-linux-ppc64le.tar.xz", "node-v20.19.0-linux-ppc64le", "84937108f005679e60b486ed8e801cebfe923f02b76d8e710463d32f82181f65"), - "20.19.0-linux_s390x": ("node-v20.19.0-linux-s390x.tar.xz", "node-v20.19.0-linux-s390x", "11f8ee99d792a83bba7b29911e0229dd6cd5e88987d7416346067db1cc76d89a"), - "20.19.0-linux_amd64": ("node-v20.19.0-linux-x64.tar.xz", "node-v20.19.0-linux-x64", "b4e336584d62abefad31baecff7af167268be9bb7dd11f1297112e6eed3ca0d5"), - "20.19.0-windows_amd64": ("node-v20.19.0-win-x64.zip", "node-v20.19.0-win-x64", "be72284c7bc62de07d5a9fd0ae196879842c085f11f7f2b60bf8864c0c9d6a4f"), - }, - node_version = "20.19.0", -) - -nodejs_register_toolchains( - name = "node22", - node_repositories = { - "22.12.0-darwin_arm64": ("node-v22.12.0-darwin-arm64.tar.gz", "node-v22.12.0-darwin-arm64", "293dcc6c2408da21562d135b0412525e381bb6fe150d688edb58fe850d0f3e13"), - "22.12.0-darwin_amd64": ("node-v22.12.0-darwin-x64.tar.gz", "node-v22.12.0-darwin-x64", "52bc25dd026db7247c3c00439afdb83e95087248267f02d6c1a7250d1f896173"), - "22.12.0-linux_arm64": ("node-v22.12.0-linux-arm64.tar.xz", "node-v22.12.0-linux-arm64", "8cfd5a8b9afae5a2e0bd86b0148ca31d2589c0ea669c2d0b11c132e35d90ed68"), - "22.12.0-linux_ppc64le": ("node-v22.12.0-linux-ppc64le.tar.xz", "node-v22.12.0-linux-ppc64le", "199a606ba1ee86cce6d6b369c71f9d00873d2836a6662592afc3b6a5923e2004"), - "22.12.0-linux_s390x": ("node-v22.12.0-linux-s390x.tar.xz", "node-v22.12.0-linux-s390x", "9b517f8006eb4b451d40c461cbe64f93c6455566dbe2613387ab02412bc06d35"), - "22.12.0-linux_amd64": ("node-v22.12.0-linux-x64.tar.xz", "node-v22.12.0-linux-x64", "22982235e1b71fa8850f82edd09cdae7e3f32df1764a9ec298c72d25ef2c164f"), - "22.12.0-windows_amd64": ("node-v22.12.0-win-x64.zip", "node-v22.12.0-win-x64", "2b8f2256382f97ad51e29ff71f702961af466c4616393f767455501e6aece9b8"), - }, - node_version = "22.12.0", -) - -nodejs_register_toolchains( - name = "node24", - node_repositories = NODE_24_REPO, - node_version = NODE_24_VERSION, -) - -load("@aspect_rules_js//js:toolchains.bzl", "rules_js_register_toolchains") - -rules_js_register_toolchains( - node_repositories = NODE_24_REPO, - node_version = NODE_24_VERSION, -) - -http_archive( - name = "aspect_bazel_lib", - sha256 = "3522895fa13b97e8b27e3b642045682aa4233ae1a6b278aad6a3b483501dc9f2", - strip_prefix = "bazel-lib-2.20.0", - url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.20.0/bazel-lib-v2.20.0.tar.gz", -) - -load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains") - -aspect_bazel_lib_dependencies() - -aspect_bazel_lib_register_toolchains() - -load("@aspect_rules_js//npm:repositories.bzl", "npm_translate_lock") - -npm_translate_lock( - name = "npm", - custom_postinstalls = { - # TODO: Standardize browser management for `rules_js` - "webdriver-manager": "node ./bin/webdriver-manager update --standalone false --gecko false --versions.chrome 106.0.5249.21", - }, - data = [ - "//:package.json", - "//:pnpm-workspace.yaml", - "//modules/testing/builder:package.json", - "//packages/angular/build:package.json", - "//packages/angular/cli:package.json", - "//packages/angular/pwa:package.json", - "//packages/angular/ssr:package.json", - "//packages/angular_devkit/architect:package.json", - "//packages/angular_devkit/architect_cli:package.json", - "//packages/angular_devkit/build_angular:package.json", - "//packages/angular_devkit/build_webpack:package.json", - "//packages/angular_devkit/core:package.json", - "//packages/angular_devkit/schematics:package.json", - "//packages/angular_devkit/schematics_cli:package.json", - "//packages/ngtools/webpack:package.json", - "//packages/schematics/angular:package.json", - "//tests:package.json", - "//tools/baseline_browserslist:package.json", - ], - lifecycle_hooks_envs = { - # TODO: Standardize browser management for `rules_js` - "puppeteer": ["PUPPETEER_DOWNLOAD_PATH=./downloads"], - }, - lifecycle_hooks_execution_requirements = { - # Needed for downloading chromedriver. - # Also `update-config` of webdriver manager would store an absolute path; - # which would then break execution. - "webdriver-manager": ["local"], - }, - npmrc = "//:.npmrc", - patches = { - # Note: Patches not needed as the existing patches are only - # for `rules_nodejs` dependencies :) - }, - pnpm_lock = "//:pnpm-lock.yaml", - public_hoist_packages = { - # TODO: Remove when https://github.com/verdaccio/verdaccio/commit/bf0e09a509e8e0a74167b0307d129202bc3f40d2 is available. - "@verdaccio/config": [""], - }, - verify_node_modules_ignored = "//:.bazelignore", -) - -load("@npm//:repositories.bzl", "npm_repositories") - -npm_repositories() - -http_archive( - name = "aspect_rules_ts", - sha256 = "09af62a0d46918d815b5f48b5ed0f5349b62c15fc42fcc3fef5c246504ff8d99", - strip_prefix = "rules_ts-3.6.3", - url = "https://github.com/aspect-build/rules_ts/releases/download/v3.6.3/rules_ts-v3.6.3.tar.gz", -) - -load("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies") - -rules_ts_dependencies( - # Obtained by: curl --silent https://registry.npmjs.org/typescript/5.9.2 | jq -r '.dist.integrity' - ts_integrity = "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - ts_version_from = "//:package.json", -) - -http_file( - name = "tsc_worker", - sha256 = "5a5c46846ecda83e05b9da26f1672ad51c59bce08fed88419850d0e29c993b30", - urls = ["https://raw.githubusercontent.com/devversion/rules_angular/4b7532ba2b29078d005899cd15b415593d03cceb/dist/worker.mjs"], -) - -http_archive( - name = "aspect_rules_jasmine", - sha256 = "0d2f9c977842685895020cac721d8cc4f1b37aae15af46128cf619741dc61529", - strip_prefix = "rules_jasmine-2.0.0", - url = "https://github.com/aspect-build/rules_jasmine/releases/download/v2.0.0/rules_jasmine-v2.0.0.tar.gz", -) - -load("@aspect_rules_jasmine//jasmine:dependencies.bzl", "rules_jasmine_dependencies") - -rules_jasmine_dependencies() - -load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") - -git_repository( - name = "devinfra", - commit = "fbdd8b7df383ae8fb34907a98353c1e8f0f5e528", - remote = "https://github.com/angular/dev-infra.git", -) - -load("@devinfra//bazel:setup_dependencies_1.bzl", "setup_dependencies_1") - -setup_dependencies_1() - -load("@devinfra//bazel:setup_dependencies_2.bzl", "setup_dependencies_2") - -setup_dependencies_2() - -register_toolchains( - "@devinfra//bazel/git-toolchain:git_linux_toolchain", - "@devinfra//bazel/git-toolchain:git_macos_x86_toolchain", - "@devinfra//bazel/git-toolchain:git_macos_arm64_toolchain", - "@devinfra//bazel/git-toolchain:git_windows_toolchain", -) - -http_archive( - name = "aspect_rules_esbuild", - sha256 = "530adfeae30bbbd097e8af845a44a04b641b680c5703b3bf885cbd384ffec779", - strip_prefix = "rules_esbuild-0.22.1", - url = "https://github.com/aspect-build/rules_esbuild/releases/download/v0.22.1/rules_esbuild-v0.22.1.tar.gz", -) - -load("@aspect_rules_esbuild//esbuild:dependencies.bzl", "rules_esbuild_dependencies") - -rules_esbuild_dependencies() - -load("@aspect_rules_esbuild//esbuild:repositories.bzl", "LATEST_ESBUILD_VERSION", "esbuild_register_toolchains") - -esbuild_register_toolchains( - name = "esbuild", - esbuild_version = LATEST_ESBUILD_VERSION, -) - -git_repository( - name = "rules_angular", - commit = "c8af5c0d27c66387e9e7df3c4dd3155ce7582609", - remote = "https://github.com/devversion/rules_angular.git", -) - -load("@rules_angular//setup:step_1.bzl", "rules_angular_step1") - -rules_angular_step1() - -load("@rules_angular//setup:step_2.bzl", "rules_angular_step2") - -rules_angular_step2() - -load("@rules_angular//setup:step_3.bzl", "rules_angular_step3") - -rules_angular_step3( - angular_compiler_cli = "//:node_modules/@angular/compiler-cli", - typescript = "//:node_modules/typescript", -) - -http_archive( - name = "aspect_rules_rollup", - sha256 = "0b8ac7d97cd660eb9a275600227e9c4268f5904cba962939d1a6ce9a0a059d2e", - strip_prefix = "rules_rollup-2.0.1", - url = "https://github.com/aspect-build/rules_rollup/releases/download/v2.0.1/rules_rollup-v2.0.1.tar.gz", -) - -git_repository( - name = "rules_browsers", - commit = "56ef8007ea07cd1916429bca8bb523433b0e9cdc", - remote = "https://github.com/devversion/rules_browsers.git", -) - -load("@rules_browsers//setup:step_1.bzl", "rules_browsers_setup_1") - -rules_browsers_setup_1() - -load("@rules_browsers//setup:step_2.bzl", "rules_browsers_setup_2") - -rules_browsers_setup_2() diff --git a/constants.bzl b/constants.bzl index 76b8d7e1dccb..04e088577dea 100644 --- a/constants.bzl +++ b/constants.bzl @@ -3,17 +3,17 @@ RELEASE_ENGINES_NODE = "^20.19.0 || ^22.12.0 || >=24.0.0" RELEASE_ENGINES_NPM = "^6.11.0 || ^7.5.6 || >=8.0.0" RELEASE_ENGINES_YARN = ">= 1.13.0" -NG_PACKAGR_VERSION = "^20.2.0-next.0" -ANGULAR_FW_VERSION = "^20.2.0-next.0" -ANGULAR_FW_PEER_DEP = "^20.0.0 || ^20.2.0-next.0" -NG_PACKAGR_PEER_DEP = "^20.0.0 || ^20.2.0-next.0" +NG_PACKAGR_VERSION = "^21.0.0-next.0" +ANGULAR_FW_VERSION = "^21.0.0-next.0" +ANGULAR_FW_PEER_DEP = "^21.0.0-next.0" +NG_PACKAGR_PEER_DEP = "^21.0.0-next.0" # Baseline widely-available date in `YYYY-MM-DD` format which defines Angular's # browser support. This date serves as the source of truth for the Angular CLI's # default browser set used to determine what downleveling is necessary. # # See: https://web.dev/baseline -BASELINE_DATE = "2025-04-30" +BASELINE_DATE = "2025-10-20" SNAPSHOT_REPOS = { "@angular/cli": "angular/cli-builds", diff --git a/docs/process/release.md b/docs/process/release.md index e8867c909ce3..1e060ac2169b 100644 --- a/docs/process/release.md +++ b/docs/process/release.md @@ -123,12 +123,15 @@ will block the next weekly release. pnpm ng-dev release build ``` 1. Log in to NPM as `angular`. + ```shell npm login ``` + - See these two Valentine entries for authentication details: - https://valentine.corp.google.com/#/show/1460636514618735 - https://valentine.corp.google.com/#/show/1531867371192103 + 1. Publish the release. ```shell (cd dist/releases/my-scope/my-pkg/ && npm publish --access public) @@ -161,17 +164,10 @@ A few weeks before a major (around feature freeze): - Picking a date at the end of a month makes it easier to cross-reference Angular's support with other tools (like MDN) which state Baseline support using month specificity. - - You can view the generated `browserlist` configuration with: - ```shell - bazel build //packages/angular/build:angular_browserslist - cat dist/bin/packages/angular/build/.browserslistrc - ``` - Commit and merge the change, no other alterations or automation is necessary in the CLI repo. -2. Update - [`/.browserslistrc`](https://github.com/ng-packagr/ng-packagr/tree/main/.browserslistrc) - in the `ng-packagr` repo. - - Use the generated configuration from above. +2. Update the date in the `ng-packagr` repo. + [`/.stylesheet-processor.ts`](https://github.com/ng-packagr/ng-packagr/blob/main/src/lib/styles/stylesheet-processor.ts#L25). 3. Update [`angular.dev` documentation](https://github.com/angular/angular/tree/main/adev/src/content/reference/versions.md#browser-support) to specify the date used and link to [browsersl.ist](https://browsersl.ist) diff --git a/eslint.config.mjs b/eslint.config.mjs index eeb7f6c60cd2..318b5dec3bee 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,19 +1,23 @@ +/** + * @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 { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; import stylistic from '@stylistic/eslint-plugin'; import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import _import from 'eslint-plugin-import'; +import tsParser from '@typescript-eslint/parser'; import header from 'eslint-plugin-header'; +import _import from 'eslint-plugin-import'; import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import js from '@eslint/js'; -import { FlatCompat } from '@eslint/eslintrc'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: import.meta.dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all, }); @@ -185,6 +189,7 @@ export default [ }, ], + '@typescript-eslint/await-thenable': 'off', '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-implied-eval': 'off', '@typescript-eslint/no-var-requires': 'off', diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index 19869c39699c..a51449319e47 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -112,6 +112,9 @@ export enum BuildOutputFileType { export type DevServerBuilderOptions = { allowedHosts?: AllowedHosts; buildTarget: string; + define?: { + [key: string]: string; + }; headers?: { [key: string]: string; }; @@ -215,19 +218,30 @@ export type NgPackagrBuilderOptions = { // @public export type UnitTestBuilderOptions = { + browserViewport?: string; browsers?: string[]; - buildTarget: string; - codeCoverage?: boolean; - codeCoverageExclude?: string[]; - codeCoverageReporters?: SchemaCodeCoverageReporter[]; + buildTarget?: string; + coverage?: boolean; + coverageExclude?: string[]; + coverageInclude?: string[]; + coverageReporters?: SchemaCoverageReporter[]; + coverageThresholds?: CoverageThresholds; + coverageWatermarks?: CoverageWatermarks; debug?: boolean; + dumpVirtualFiles?: boolean; exclude?: string[]; + filter?: string; include?: string[]; + listTests?: boolean; + outputFile?: string; + progress?: boolean; providersFile?: string; - reporters?: string[]; - runner: Runner; + reporters?: SchemaReporter[]; + runner?: Runner; + runnerConfig?: RunnerConfig; setupFiles?: string[]; - tsConfig: string; + tsConfig?: string; + ui?: boolean; watch?: boolean; }; diff --git a/goldens/public-api/angular/ssr/node/index.api.md b/goldens/public-api/angular/ssr/node/index.api.md index 0f30fa7e99c5..eccb6396938e 100644 --- a/goldens/public-api/angular/ssr/node/index.api.md +++ b/goldens/public-api/angular/ssr/node/index.api.md @@ -5,6 +5,7 @@ ```ts import { ApplicationRef } from '@angular/core'; +import { BootstrapContext } from '@angular/platform-browser'; import { Http2ServerRequest } from 'node:http2'; import { Http2ServerResponse } from 'node:http2'; import { IncomingMessage } from 'node:http'; @@ -26,14 +27,14 @@ export class CommonEngine { // @public (undocumented) export interface CommonEngineOptions { - bootstrap?: Type<{}> | (() => Promise); + bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise); enablePerformanceProfiler?: boolean; providers?: StaticProvider[]; } // @public (undocumented) export interface CommonEngineRenderOptions { - bootstrap?: Type<{}> | (() => Promise); + bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise); // (undocumented) document?: string; // (undocumented) diff --git a/goldens/public-api/manage.js b/goldens/public-api/manage.js deleted file mode 100644 index 569f2a6bd1e3..000000000000 --- a/goldens/public-api/manage.js +++ /dev/null @@ -1,54 +0,0 @@ -const {exec} = require('shelljs'); -const {Parser: parser} = require('yargs/helpers'); - -// Remove all command line flags from the arguments. -const argv = parser(process.argv.slice(2)); -// The command the user would like to run, either 'accept' or 'test' -const USER_COMMAND = argv._[0]; -// The shell command to query for all Public API guard tests. -const BAZEL_PUBLIC_API_TARGET_QUERY_CMD = - `pnpm -s bazel query --output label 'kind(nodejs_test, ...) intersect attr("tags", "api_guard", ...)'` -// Bazel targets for testing Public API goldens -process.stdout.write('Gathering all Public API targets'); -const ALL_PUBLIC_API_TESTS = exec(BAZEL_PUBLIC_API_TARGET_QUERY_CMD, {silent: true}) - .trim() - .split('\n') - .map(test => test.trim()); -process.stdout.clearLine(); -process.stdout.cursorTo(0); -// Bazel targets for generating Public API goldens -const ALL_PUBLIC_API_ACCEPTS = ALL_PUBLIC_API_TESTS.map(test => `${test}.accept`); - -/** - * Run the provided bazel commands on each provided target individually. - */ -function runBazelCommandOnTargets(command, targets, present) { - for (const target of targets) { - process.stdout.write(`${present}: ${target}`); - const commandResult = exec(`pnpm -s bazel ${command} ${target}`, {silent: true}); - process.stdout.clearLine(); - process.stdout.cursorTo(0); - if (commandResult.code) { - console.error(`Failed ${command}: ${target}`); - console.group(); - console.error(commandResult.stdout || commandResult.stderr); - console.groupEnd(); - } else { - console.log(`Successful ${command}: ${target}`); - } - } -} - -switch (USER_COMMAND) { - case 'accept': - runBazelCommandOnTargets('run', ALL_PUBLIC_API_ACCEPTS, 'Running'); - break; - case 'test': - runBazelCommandOnTargets('test', ALL_PUBLIC_API_TESTS, 'Testing'); - break; - default: - console.warn('Invalid command provided.'); - console.warn(); - console.warn(`Run this script with either "accept" and "test"`); - break; -} diff --git a/modules/testing/builder/BUILD.bazel b/modules/testing/builder/BUILD.bazel index 154f494af35f..9b88a714cd05 100644 --- a/modules/testing/builder/BUILD.bazel +++ b/modules/testing/builder/BUILD.bazel @@ -20,7 +20,9 @@ ts_project( # Needed at runtime by some builder tests relying on SSR being # resolvable in the test project. ":node_modules/@angular/ssr", + ":node_modules/jsdom", ":node_modules/vitest", + ":node_modules/@vitest/coverage-v8", ] + glob(["projects/**/*"]), deps = [ ":node_modules/@angular-devkit/architect", diff --git a/modules/testing/builder/package.json b/modules/testing/builder/package.json index 6d5b445b0c8e..532534e75a5c 100644 --- a/modules/testing/builder/package.json +++ b/modules/testing/builder/package.json @@ -4,7 +4,9 @@ "@angular-devkit/architect": "workspace:*", "@angular/ssr": "workspace:*", "@angular-devkit/build-angular": "workspace:*", + "@vitest/coverage-v8": "4.0.7", + "jsdom": "27.1.0", "rxjs": "7.8.2", - "vitest": "3.2.4" + "vitest": "4.0.7" } } diff --git a/modules/testing/builder/projects/hello-world-app/tsconfig.json b/modules/testing/builder/projects/hello-world-app/tsconfig.json index 8019279a7006..8f8859a21d4f 100644 --- a/modules/testing/builder/projects/hello-world-app/tsconfig.json +++ b/modules/testing/builder/projects/hello-world-app/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, - "moduleResolution": "node", + "moduleResolution": "bundler", "emitDecoratorMetadata": true, "experimentalDecorators": true, "skipLibCheck": true, diff --git a/modules/testing/builder/src/builder-harness.ts b/modules/testing/builder/src/builder-harness.ts index ec4973efa021..67b5f760d148 100644 --- a/modules/testing/builder/src/builder-harness.ts +++ b/modules/testing/builder/src/builder-harness.ts @@ -162,6 +162,24 @@ export class BuilderHarness { return this; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + modifyTarget( + targetName: string, + modifier: (options: O) => O | void, + ): this { + const target = this.builderTargets.get(targetName); + if (!target) { + throw new Error(`Target "${targetName}" not found.`); + } + + const newOptions = modifier(target.options as O); + if (newOptions) { + target.options = newOptions as json.JsonObject; + } + + return this; + } + execute( options: Partial = {}, ): Observable { diff --git a/modules/testing/builder/src/index.ts b/modules/testing/builder/src/index.ts index 8f4687f9fdca..3ee4220781d9 100644 --- a/modules/testing/builder/src/index.ts +++ b/modules/testing/builder/src/index.ts @@ -15,5 +15,7 @@ export { type HarnessFileMatchers, JasmineBuilderHarness, describeBuilder, + expectLog, + expectNoLog, } from './jasmine-helpers'; export * from './test-utils'; diff --git a/modules/testing/builder/src/jasmine-helpers.ts b/modules/testing/builder/src/jasmine-helpers.ts index 94fdbeb38fe1..b31f2e273333 100644 --- a/modules/testing/builder/src/jasmine-helpers.ts +++ b/modules/testing/builder/src/jasmine-helpers.ts @@ -7,7 +7,7 @@ */ import { BuilderHandlerFn } from '@angular-devkit/architect'; -import { json } from '@angular-devkit/core'; +import { json, logging } from '@angular-devkit/core'; import { readFileSync } from 'node:fs'; import { concatMap, count, debounceTime, firstValueFrom, take, timeout } from 'rxjs'; import { @@ -196,3 +196,25 @@ export function expectDirectory( }, }; } + +export function expectLog(logs: readonly logging.LogEntry[], message: string | RegExp) { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(message), + }), + ); +} + +export function expectNoLog( + logs: readonly logging.LogEntry[], + message: string | RegExp, + failureMessage?: string, +) { + expect(logs) + .withContext(failureMessage ?? '') + .not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(message), + }), + ); +} diff --git a/package.json b/package.json index e1790e8c7e32..a04ea47beead 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.2.0-next.3", + "version": "21.1.0-next.0", "private": true, "description": "Software Development Kit for Angular", "keywords": [ @@ -21,9 +21,7 @@ "postinstall": "pnpm -s webdriver-update && husky", "//webdriver-update-README": "ChromeDriver version must match Puppeteer Chromium version, see https://github.com/GoogleChrome/puppeteer/releases http://chromedriver.chromium.org/downloads", "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 106.0.5249.21", - "public-api:check": "node goldens/public-api/manage.js test", "ng-dev": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only node_modules/@angular/ng-dev/bundles/cli.mjs", - "public-api:update": "node goldens/public-api/manage.js accept", "ts-circular-deps": "pnpm -s ng-dev ts-circular-deps --config ./scripts/circular-deps-test.conf.mjs", "check-tooling-setup": "tsc --project .ng-dev/tsconfig.json", "diff-release-package": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only scripts/diff-release-package.mts" @@ -32,12 +30,12 @@ "type": "git", "url": "https://github.com/angular/angular-cli.git" }, - "packageManager": "pnpm@10.14.0", + "packageManager": "pnpm@10.20.0", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "Please use pnpm instead of NPM to install dependencies", "yarn": "Please use pnpm instead of Yarn to install dependencies", - "pnpm": "^10.0.0" + "pnpm": "10.20.0" }, "author": "Angular Authors", "license": "MIT", @@ -46,29 +44,30 @@ }, "homepage": "https://github.com/angular/angular-cli", "devDependencies": { - "@angular/animations": "20.2.0-rc.0", - "@angular/cdk": "20.2.0-next.3", - "@angular/common": "20.2.0-rc.0", - "@angular/compiler": "20.2.0-rc.0", - "@angular/compiler-cli": "20.2.0-rc.0", - "@angular/core": "20.2.0-rc.0", - "@angular/forms": "20.2.0-rc.0", - "@angular/localize": "20.2.0-rc.0", - "@angular/material": "20.2.0-next.3", - "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#e16e229975bd41d66ec49905d5896b8f61068a19", - "@angular/platform-browser": "20.2.0-rc.0", - "@angular/platform-server": "20.2.0-rc.0", - "@angular/router": "20.2.0-rc.0", - "@angular/service-worker": "20.2.0-rc.0", + "@angular/animations": "21.0.0-rc.0", + "@angular/cdk": "21.0.0-rc.0", + "@angular/common": "21.0.0-rc.0", + "@angular/compiler": "21.0.0-rc.0", + "@angular/compiler-cli": "21.0.0-rc.0", + "@angular/core": "21.0.0-rc.0", + "@angular/forms": "21.0.0-rc.0", + "@angular/localize": "21.0.0-rc.0", + "@angular/material": "21.0.0-rc.0", + "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#2a91d0e8e153f50fc072455c6dd52ac3fea664e6", + "@angular/platform-browser": "21.0.0-rc.0", + "@angular/platform-server": "21.0.0-rc.0", + "@angular/router": "21.0.0-rc.0", + "@angular/service-worker": "21.0.0-rc.0", + "@babel/core": "7.28.5", "@bazel/bazelisk": "1.26.0", "@bazel/buildifier": "8.2.1", - "@eslint/compat": "1.3.2", + "@eslint/compat": "1.4.1", "@eslint/eslintrc": "3.3.1", - "@eslint/js": "9.33.0", - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.0", + "@eslint/js": "9.39.1", + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "16.0.1", + "@rollup/plugin-node-resolve": "16.0.3", "@stylistic/eslint-plugin": "^5.0.0", "@types/babel__core": "7.20.5", "@types/babel__generator": "^7.6.8", @@ -80,7 +79,7 @@ "@types/jasmine-reporters": "^2", "@types/karma": "^6.3.0", "@types/less": "^3.0.3", - "@types/loader-utils": "^2.0.0", + "@types/loader-utils": "^3.0.0", "@types/lodash": "^4.17.0", "@types/node": "^22.12.0", "@types/npm-package-arg": "^6.1.0", @@ -89,31 +88,29 @@ "@types/progress": "^2.0.3", "@types/resolve": "^1.17.1", "@types/semver": "^7.3.12", - "@types/shelljs": "^0.8.11", "@types/watchpack": "^2.4.4", "@types/yargs": "^17.0.20", "@types/yargs-parser": "^21.0.0", "@types/yarnpkg__lockfile": "^1.1.5", - "@typescript-eslint/eslint-plugin": "8.39.1", - "@typescript-eslint/parser": "8.39.1", + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", "ajv": "8.17.1", "ansi-colors": "4.1.3", - "beasties": "0.3.5", "buffer": "6.0.3", - "esbuild": "0.25.9", - "esbuild-wasm": "0.25.9", - "eslint": "9.33.0", + "esbuild": "0.25.12", + "esbuild-wasm": "0.25.12", + "eslint": "9.39.1", "eslint-config-prettier": "10.1.8", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.32.0", "express": "5.1.0", "fast-glob": "3.3.3", - "globals": "16.3.0", + "globals": "16.5.0", "http-proxy": "^1.18.1", "http-proxy-middleware": "3.0.5", "husky": "9.1.7", - "jasmine": "~5.9.0", - "jasmine-core": "~5.9.0", + "jasmine": "~5.12.0", + "jasmine-core": "~5.12.0", "jasmine-reporters": "^2.5.2", "jasmine-spec-reporter": "~7.0.0", "karma": "~6.4.0", @@ -122,30 +119,29 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "karma-source-map-support": "1.4.0", - "listr2": "9.0.1", + "listr2": "9.0.5", "lodash": "^4.17.21", - "npm": "^11.0.0", - "magic-string": "0.30.17", - "rollup-plugin-dts": "6.2.1", - "rollup-plugin-sourcemaps2": "0.5.3", + "magic-string": "0.30.21", "prettier": "^3.0.0", "protractor": "~7.0.0", "puppeteer": "18.2.1", "quicktype-core": "23.2.6", - "rollup": "4.46.2", + "rollup": "4.52.5", "rollup-license-plugin": "~3.0.1", - "semver": "7.7.2", - "shelljs": "^0.10.0", + "rollup-plugin-dts": "6.2.3", + "rollup-plugin-sourcemaps2": "0.5.4", + "semver": "7.7.3", "source-map-support": "0.5.21", "tar": "^7.0.0", "ts-node": "^10.9.1", "tslib": "2.8.1", - "typescript": "5.9.2", - "undici": "7.13.0", + "typescript": "5.9.3", + "undici": "7.16.0", "unenv": "^1.10.0", - "verdaccio": "6.1.6", + "verdaccio": "6.2.1", "verdaccio-auth-memory": "^10.0.0", "yargs-parser": "22.0.0", + "zod": "4.1.12", "zone.js": "^0.15.0" }, "dependenciesMeta": { @@ -173,6 +169,7 @@ } }, "resolutions": { - "typescript": "5.9.2" + "typescript": "5.9.3", + "undici-types": "^7.16.0" } } diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index fbbe5e86ee12..d8f89be1d5cd 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -1,9 +1,7 @@ load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") -load("//:constants.bzl", "BASELINE_DATE") load("//tools:defaults.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") -load("//tools/baseline_browserslist:baseline_browserslist.bzl", "baseline_browserslist") licenses(["notice"]) @@ -46,23 +44,14 @@ copy_to_bin( srcs = glob(["**/schema.json"]), ) -baseline_browserslist( - name = "angular_browserslist", - out = ".browserslistrc", - baseline = BASELINE_DATE, -) - RUNTIME_ASSETS = glob( include = [ "src/**/schema.json", "src/**/*.js", - "src/**/*.mjs", - "src/**/*.html", ], ) + [ "builders.json", "package.json", - ":angular_browserslist", ] ts_project( @@ -97,6 +86,7 @@ ts_project( ":node_modules/@babel/helper-split-export-declaration", ":node_modules/@inquirer/confirm", ":node_modules/@vitejs/plugin-basic-ssl", + ":node_modules/beasties", ":node_modules/browserslist", ":node_modules/https-proxy-agent", ":node_modules/istanbul-lib-instrument", @@ -115,6 +105,7 @@ ts_project( ":node_modules/sass", ":node_modules/source-map-support", ":node_modules/tinyglobby", + ":node_modules/undici", ":node_modules/vite", ":node_modules/vitest", ":node_modules/watchpack", @@ -123,6 +114,7 @@ ts_project( "//:node_modules/@angular/compiler-cli", "//:node_modules/@angular/core", "//:node_modules/@angular/localize", + "//:node_modules/@angular/platform-browser", "//:node_modules/@angular/platform-server", "//:node_modules/@angular/service-worker", "//:node_modules/@types/babel__core", @@ -132,7 +124,6 @@ ts_project( "//:node_modules/@types/picomatch", "//:node_modules/@types/semver", "//:node_modules/@types/watchpack", - "//:node_modules/beasties", "//:node_modules/esbuild", "//:node_modules/esbuild-wasm", "//:node_modules/karma", @@ -320,8 +311,9 @@ jasmine_test( jasmine_test( name = "unit-test_integration_tests", - size = "small", + size = "medium", data = [":unit-test_integration_test_lib"], + flaky = True, shard_count = 5, ) @@ -341,6 +333,7 @@ npm_package( "src/utils/version.js", "src/tools/esbuild/utils.js", "src/utils/normalize-cache.js", + "src/utils/supported-browsers.js", ], tags = ["release-package"], deps = RUNTIME_ASSETS + [ diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 2491bace216e..aaefb19bd7b9 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -20,48 +20,49 @@ "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER", - "@babel/core": "7.28.0", + "@babel/core": "7.28.5", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.14", + "@inquirer/confirm": "5.1.19", "@vitejs/plugin-basic-ssl": "2.1.0", "beasties": "0.3.5", - "browserslist": "^4.23.0", - "esbuild": "0.25.9", + "browserslist": "^4.26.0", + "esbuild": "0.25.12", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", - "listr2": "9.0.1", - "magic-string": "0.30.17", + "listr2": "9.0.5", + "magic-string": "0.30.21", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", "piscina": "5.1.3", - "rolldown": "1.0.0-beta.32", - "sass": "1.90.0", - "semver": "7.7.2", + "rolldown": "1.0.0-beta.47", + "sass": "1.93.3", + "semver": "7.7.3", "source-map-support": "0.5.21", - "tinyglobby": "0.2.14", - "vite": "7.1.2", + "tinyglobby": "0.2.15", + "undici": "7.16.0", + "vite": "7.2.1", "watchpack": "2.4.4" }, "optionalDependencies": { - "lmdb": "3.4.2" + "lmdb": "3.4.3" }, "devDependencies": { - "@angular/ssr": "workspace:*", "@angular-devkit/core": "workspace:*", - "jsdom": "26.1.0", - "less": "4.4.0", - "ng-packagr": "20.2.0-next.1", + "@angular/ssr": "workspace:*", + "jsdom": "27.1.0", + "less": "4.4.2", + "ng-packagr": "21.0.0-rc.0", "postcss": "8.5.6", "rxjs": "7.8.2", - "vitest": "3.2.4" + "vitest": "4.0.7" }, "peerDependencies": { - "@angular/core": "0.0.0-ANGULAR-FW-PEER-DEP", "@angular/compiler": "0.0.0-ANGULAR-FW-PEER-DEP", "@angular/compiler-cli": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/core": "0.0.0-ANGULAR-FW-PEER-DEP", "@angular/localize": "0.0.0-ANGULAR-FW-PEER-DEP", "@angular/platform-browser": "0.0.0-ANGULAR-FW-PEER-DEP", "@angular/platform-server": "0.0.0-ANGULAR-FW-PEER-DEP", @@ -73,8 +74,8 @@ "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.8 <6.0", - "vitest": "^3.1.1" + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.6" }, "peerDependenciesMeta": { "@angular/core": { diff --git a/packages/angular/build/src/builders/application/chunk-optimizer.ts b/packages/angular/build/src/builders/application/chunk-optimizer.ts index 4e37a11e03f9..e6827479b784 100644 --- a/packages/angular/build/src/builders/application/chunk-optimizer.ts +++ b/packages/angular/build/src/builders/application/chunk-optimizer.ts @@ -252,7 +252,7 @@ export async function optimizeChunks( }); const result = await bundle.generate({ - minify: { mangle: false, compress: false, removeWhitespace: true }, + minify: { mangle: false, compress: false }, sourcemap, chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`, }); diff --git a/packages/angular/build/src/builders/application/index.ts b/packages/angular/build/src/builders/application/index.ts index 80261c41277f..b83e3b48f270 100644 --- a/packages/angular/build/src/builders/application/index.ts +++ b/packages/angular/build/src/builders/application/index.ts @@ -14,7 +14,7 @@ import { BuildOutputFileType } from '../../tools/esbuild/bundler-context'; import { createJsonBuildManifest, emitFilesToDisk } from '../../tools/esbuild/utils'; import { colors as ansiColors } from '../../utils/color'; import { deleteOutputDir } from '../../utils/delete-output-dir'; -import { useJSONBuildLogs } from '../../utils/environment-options'; +import { bazelEsbuildPluginPath, useJSONBuildLogs } from '../../utils/environment-options'; import { purgeStaleBuildCache } from '../../utils/purge-cache'; import { assertCompatibleAngularVersion } from '../../utils/version'; import { runEsBuildBuildAction } from './build-action'; @@ -56,6 +56,14 @@ export async function* buildApplicationInternal( return; } + if (bazelEsbuildPluginPath) { + extensions ??= {}; + extensions.codePlugins ??= []; + + const { default: bazelEsbuildPlugin } = await import(bazelEsbuildPluginPath); + extensions.codePlugins.push(bazelEsbuildPlugin); + } + const normalizedOptions = await normalizeOptions(context, projectName, options, extensions); if (!normalizedOptions.outputOptions.ignoreServer) { @@ -109,7 +117,8 @@ export async function* buildApplicationInternal( const hasError = result.errors.length > 0; result.addLog( - `Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`, + `Application bundle generation ${hasError ? 'failed' : 'complete'}.` + + ` [${buildTime.toFixed(3)} seconds] - ${new Date().toISOString()}\n`, ); } diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 3ee8699e097f..8db4e6145b3f 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -6,7 +6,7 @@ "properties": { "assets": { "type": "array", - "description": "List of static application assets.", + "description": "Define the assets to be copied to the output directory. These assets are copied as-is without any further processing or hashing.", "default": [], "items": { "$ref": "#/definitions/assetPattern" @@ -441,7 +441,7 @@ }, "outputHashing": { "type": "string", - "description": "Define the output filename cache-busting hashing mode.", + "description": "Define the output filename cache-busting hashing mode.\n\n- `none`: No hashing.\n- `all`: Hash for all output bundles. \n- `media`: Hash for all output media (e.g., images, fonts, etc. that are referenced in CSS files).\n- `bundles`: Hash for output of lazy and main bundles.", "default": "none", "enum": ["none", "all", "media", "bundles"] }, @@ -611,7 +611,7 @@ }, "outputMode": { "type": "string", - "description": "Defines the build output target. 'static': Generates a static site for deployment on any static hosting service. 'server': Produces an application designed for deployment on a server that supports server-side rendering (SSR).", + "description": "Defines the type of build output artifact. 'static': Generates a static site build artifact for deployment on any static hosting service. 'server': Generates a server application build artifact, required for applications using hybrid rendering or APIs.", "enum": ["static", "server"] } }, diff --git a/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts index ecc460bcb405..ddae750a64a4 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts @@ -11,7 +11,7 @@ import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setu describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Component Stylesheets"', () => { - it('should successfuly compile with an empty inline style', async () => { + it('should successfully compile with an empty inline style', async () => { await harness.modifyFile('src/app/app.component.ts', (content) => { return content.replace('styleUrls', 'styles').replace('./app.component.css', ''); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts index e2105fd13de7..fa384be88080 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts @@ -6,9 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { logging } from '@angular-devkit/core'; import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { + APPLICATION_BUILDER_INFO, + BASE_OPTIONS, + describeBuilder, + expectLog, + expectNoLog, +} from '../setup'; /** * Maximum time in milliseconds for single build/rebuild @@ -92,11 +97,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result, logs }) => { expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); + expectLog(logs, typeErrorText); // Make an unrelated change to verify error cache was updated // Should persist error in the next rebuild @@ -104,11 +105,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result, logs }) => { expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); + expectLog(logs, typeErrorText); // Revert the directive change that caused the error // Should remove the error @@ -116,11 +113,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result, logs }) => { expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); + expectNoLog(logs, typeErrorText); // Make an unrelated change to verify error cache was updated // Should continue showing no error @@ -128,11 +121,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, ({ result, logs }) => { expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); + expectNoLog(logs, typeErrorText); }, ], { outputLogsOnFailure: false }, @@ -152,78 +141,38 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.appendToFile('src/app/app.component.html', '@if-one'); }, async ({ logs }) => { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-one'), - }), - ); + expectLog(logs, '@if-one'); // Make an unrelated change to verify error cache was updated // Should persist error in the next rebuild await harness.modifyFile('src/main.ts', (content) => content + '\n'); }, async ({ logs }) => { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-one'), - }), - ); + expectLog(logs, '@if-one'); // Add more invalid block syntax await harness.appendToFile('src/app/app.component.html', '@if-two'); }, async ({ logs }) => { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-one'), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-two'), - }), - ); + expectLog(logs, '@if-one'); + expectLog(logs, '@if-two'); // Add more invalid block syntax await harness.appendToFile('src/app/app.component.html', '@if-three'); }, async ({ logs }) => { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-one'), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-two'), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-three'), - }), - ); + expectLog(logs, '@if-one'); + expectLog(logs, '@if-two'); + expectLog(logs, '@if-three'); // Revert the changes that caused the error // Should remove the error await harness.writeFile('src/app/app.component.html', '

GOOD

'); }, ({ logs }) => { - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-one'), - }), - ); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-two'), - }), - ); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@if-three'), - }), - ); + expectNoLog(logs, '@if-one'); + expectNoLog(logs, '@if-two'); + expectNoLog(logs, '@if-three'); }, ], { outputLogsOnFailure: false }, @@ -243,20 +192,12 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); }, async ({ logs }) => { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); + expectLog(logs, 'invalid-css-content'); await harness.writeFile('src/app/app.component.css', 'p { color: green }'); }, ({ logs }) => { - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); + expectNoLog(logs, 'invalid-css-content'); harness .expectFile('dist/browser/main.js') @@ -280,20 +221,12 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.appendToFile('src/app/app.component.html', '
Hello, world! { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); + expectLog(logs, 'Unexpected character "EOF"'); await harness.appendToFile('src/app/app.component.html', '>'); }, async ({ logs }) => { - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); + expectNoLog(logs, 'Unexpected character "EOF"'); harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); @@ -301,11 +234,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.appendToFile('src/app/app.component.html', '
Guten Tag
'); }, ({ logs }) => { - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); + expectNoLog(logs, 'invalid-css-content'); harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); harness.expectFile('dist/browser/main.js').content.toContain('Guten Tag'); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts index 4e167f2994c6..2fdad10f8d8d 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts @@ -6,9 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { logging } from '@angular-devkit/core'; import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { + APPLICATION_BUILDER_INFO, + BASE_OPTIONS, + describeBuilder, + expectLog, + expectNoLog, +} from '../setup'; /** * A regular expression used to check if a built worker is correctly referenced in application code. @@ -62,22 +67,14 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result, logs }) => { expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); + expectLog(logs, errorText); // Make an unrelated change to verify error cache was updated // Should persist error in the next rebuild await harness.modifyFile('src/main.ts', (content) => content + '\n'); }, async ({ logs }) => { - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); + expectLog(logs, errorText); // Revert the change that caused the error // Should remove the error @@ -85,11 +82,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result, logs }) => { expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); + expectNoLog(logs, errorText); // Make an unrelated change to verify error cache was updated // Should continue showing no error @@ -97,11 +90,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, ({ result, logs }) => { expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); + expectNoLog(logs, errorText); // Ensure built worker is referenced in the application code harness.expectFile('dist/browser/main.js').content.toMatch(REFERENCED_WORKER_REGEXP); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts index 1f1efafaf3c5..ba01e2a27dce 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { logging } from '@angular-devkit/core'; import { buildApplication } from '../../index'; import { OutputHashing } from '../../schema'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectLog } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { beforeEach(async () => { @@ -57,13 +56,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, async ({ result, logs }) => { expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching( - `Type 'number' is not assignable to type 'string'.`, - ), - }), - ); + expectLog(logs, `Type 'number' is not assignable to type 'string'.`); // Fix TS error await harness.writeFile('src/lazy.ts', `export const foo: string = "1";`); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts index e7d060de1262..cf21d5545f7a 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts @@ -20,6 +20,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { // Enable tsconfig resolveJsonModule option in tsconfig await harness.modifyFile('tsconfig.json', (content) => { const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.moduleResolution = 'node'; tsconfig.compilerOptions.resolveJsonModule = true; return JSON.stringify(tsconfig); @@ -43,6 +44,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { // Enable tsconfig resolveJsonModule option in tsconfig await harness.modifyFile('tsconfig.json', (content) => { const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.moduleResolution = 'node'; tsconfig.compilerOptions.resolveJsonModule = undefined; return JSON.stringify(tsconfig); @@ -71,6 +73,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { // Enable tsconfig resolveJsonModule option in tsconfig await harness.modifyFile('tsconfig.json', (content) => { const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.moduleResolution = 'node'; tsconfig.compilerOptions.resolveJsonModule = false; return JSON.stringify(tsconfig); diff --git a/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts b/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts index ad29d985f712..bcc361ccdbe1 100644 --- a/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts @@ -6,9 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { logging } from '@angular-devkit/core'; import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { + APPLICATION_BUILDER_INFO, + BASE_OPTIONS, + describeBuilder, + expectLog, + expectNoLog, +} from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "allowedCommonJsDependencies"', () => { @@ -30,22 +35,11 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching( - /Module 'buffer' used by 'src\/app\/app\.component\.ts' is not ESM/, - ), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('base64-js'), - }), + expectLog(logs, /Module 'buffer' used by 'src\/app\/app\.component\.ts' is not ESM/); + expectLog(logs, /CommonJS or AMD dependencies/); + expectNoLog( + logs, + 'base64-js', 'Should not warn on transitive CommonJS packages which parent is also CommonJS.', ); }); @@ -70,11 +64,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); + expectNoLog(logs, /CommonJS or AMD dependencies/); }); it('should not show warning when all dependencies are allowed by wildcard', async () => { @@ -95,11 +85,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); + expectNoLog(logs, /CommonJS or AMD dependencies/); }); it('should not show warning when depending on zone.js', async () => { @@ -120,11 +106,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); + expectNoLog(logs, /CommonJS or AMD dependencies/); }); it(`should not show warning when importing non global local data '@angular/common/locale/fr'`, async () => { @@ -142,11 +124,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); + expectNoLog(logs, /CommonJS or AMD dependencies/); }); it('should not show warning in JIT for templateUrl and styleUrl when using paths', async () => { @@ -178,11 +156,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); + expectNoLog(logs, /CommonJS or AMD dependencies/); }); it('should not show warning for relative imports', async () => { @@ -198,11 +172,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/CommonJS or AMD dependencies/), - }), - ); + expectNoLog(logs, /CommonJS or AMD dependencies/); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/bundle-budgets_spec.ts b/packages/angular/build/src/builders/application/tests/options/bundle-budgets_spec.ts index 4614d5a5788e..40757365ce8e 100644 --- a/packages/angular/build/src/builders/application/tests/options/bundle-budgets_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/bundle-budgets_spec.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { logging } from '@angular-devkit/core'; import { buildApplication } from '../../index'; import { Type } from '../../schema'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, + expectLog, + expectNoLog, lazyModuleFiles, lazyModuleFnImport, } from '../setup'; @@ -31,12 +32,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - level: 'warn', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectNoLog(logs, BUDGET_NOT_MET_REGEXP); }); it(`should error when size is above 'maximumError' threshold`, async () => { @@ -48,12 +44,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectLog(logs, BUDGET_NOT_MET_REGEXP); }); it(`should warn when size is above 'maximumWarning' threshold`, async () => { @@ -65,12 +56,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ - level: 'warn', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectLog(logs, BUDGET_NOT_MET_REGEXP); }); it(`should warn when lazy bundle is above 'maximumWarning' threshold`, async () => { @@ -85,12 +71,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ - level: 'warn', - message: jasmine.stringMatching('lazy-module exceeded maximum budget'), - }), - ); + expectLog(logs, 'lazy-module exceeded maximum budget'); }); it(`should not warn when non-injected style is not within the baseline threshold`, async () => { @@ -118,12 +99,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - level: 'warn', - message: jasmine.stringMatching('lazy-styles failed to meet minimum budget'), - }), - ); + expectNoLog(logs, 'lazy-styles failed to meet minimum budget'); }); CSS_EXTENSIONS.forEach((ext) => { @@ -154,12 +130,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ - level: 'warn', - message: jasmine.stringMatching(new RegExp(`app.component.${ext}`)), - }), - ); + expectLog(logs, new RegExp(`app.component.${ext}`)); }); }); @@ -175,12 +146,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectNoLog(logs, BUDGET_NOT_MET_REGEXP); }); it(`when 'intial' budget`, async () => { @@ -194,12 +160,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectNoLog(logs, BUDGET_NOT_MET_REGEXP); }); it(`when 'all' budget`, async () => { @@ -213,12 +174,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectNoLog(logs, BUDGET_NOT_MET_REGEXP); }); it(`when 'any' budget`, async () => { @@ -232,12 +188,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBe(true); - expect(logs).not.toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), - }), - ); + expectNoLog(logs, BUDGET_NOT_MET_REGEXP); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts b/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts index feb9b6447c3b..deb55e172109 100644 --- a/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts @@ -24,7 +24,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { .content.not.toMatch(/from ['"]@angular\/common['"]/); }); - it('should only externalize the listed depedencies when option is set', async () => { + it('should only externalize the listed dependencies when option is set', async () => { harness.useTarget('build', { ...BASE_OPTIONS, externalDependencies: ['@angular/core'], @@ -39,7 +39,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { .content.not.toMatch(/from ['"]@angular\/common['"]/); }); - it('should externalize the listed depedencies in Web Workers when option is set', async () => { + it('should externalize the listed dependencies in Web Workers when option is set', async () => { harness.useTarget('build', { ...BASE_OPTIONS, externalDependencies: ['path'], diff --git a/packages/angular/build/src/builders/application/tests/options/i18n-missing-translation_spec.ts b/packages/angular/build/src/builders/application/tests/options/i18n-missing-translation_spec.ts index d29c0a84adbc..90ac8206a1b8 100644 --- a/packages/angular/build/src/builders/application/tests/options/i18n-missing-translation_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/i18n-missing-translation_spec.ts @@ -7,7 +7,7 @@ */ import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectNoLog } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "i18nMissingTranslation"', () => { @@ -131,11 +131,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('No translation found for'), - }), - ); + expectNoLog(logs, 'No translation found for'); }); it('should not error or warn when i18nMissingTranslation is set to error and all found', async () => { @@ -158,11 +154,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('No translation found for'), - }), - ); + expectNoLog(logs, 'No translation found for'); }); it('should not error or warn when i18nMissingTranslation is set to warning and all found', async () => { @@ -185,11 +177,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('No translation found for'), - }), - ); + expectNoLog(logs, 'No translation found for'); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/scripts_spec.ts b/packages/angular/build/src/builders/application/tests/options/scripts_spec.ts index 757ff81acbac..dde35f851159 100644 --- a/packages/angular/build/src/builders/application/tests/options/scripts_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/scripts_spec.ts @@ -7,7 +7,7 @@ */ import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectLog } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "scripts"', () => { @@ -145,9 +145,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/scripts\.js.+\d+ bytes/) }), - ); + expectLog(logs, /scripts\.js.+\d+ bytes/); }); }); @@ -412,9 +410,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/scripts\.js.+\d+ bytes/) }), - ); + expectLog(logs, /scripts\.js.+\d+ bytes/); }); it('shows the output script as a chunk entry with bundleName in the logging output', async () => { @@ -429,9 +425,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/extra\.js.+\d+ bytes/) }), - ); + expectLog(logs, /extra\.js.+\d+ bytes/); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_spec.ts b/packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_spec.ts index 33c1d1cc9a4b..a3da36606825 100644 --- a/packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_spec.ts @@ -7,8 +7,7 @@ */ import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -import { logging } from '@angular-devkit/core'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectNoLog } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "stylePreprocessorOptions.sass"', () => { @@ -28,11 +27,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); expect(result?.success).toBeFalse(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('darken() is deprecated'), - }), - ); + expectNoLog(logs, 'darken() is deprecated'); }); it('should succeed without `fatalDeprecations` despite using deprecated color functions', async () => { @@ -79,11 +74,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }); expect(result?.success).toBeFalse(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('darken() is deprecated'), - }), - ); + expectNoLog(logs, 'darken() is deprecated'); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/styles_spec.ts b/packages/angular/build/src/builders/application/tests/options/styles_spec.ts index eb8d973ae904..311b0037daa5 100644 --- a/packages/angular/build/src/builders/application/tests/options/styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/styles_spec.ts @@ -7,7 +7,7 @@ */ import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectLog } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "styles"', () => { @@ -147,9 +147,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/styles\.css.+\d+ bytes/) }), - ); + expectLog(logs, /styles\.css.+\d+ bytes/); }); }); @@ -440,9 +438,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/styles\.css.+\d+ bytes/) }), - ); + expectLog(logs, /styles\.css.+\d+ bytes/); }); it('shows the output style as a chunk entry with bundleName in the logging output', async () => { @@ -457,9 +453,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/extra\.css.+\d+ bytes/) }), - ); + expectLog(logs, /extra\.css.+\d+ bytes/); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts index 4afb87ebaed3..f3ec9476b21f 100644 --- a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts @@ -6,9 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { logging } from '@angular-devkit/core'; import { buildApplication } from '../../index'; -import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectNoLog } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Option: "subresourceIntegrity"', () => { @@ -64,11 +63,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness .expectFile('dist/browser/index.html') .content.toMatch(/integrity="\w+-[A-Za-z0-9/+=]+"/); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/subresource-integrity/), - }), - ); + expectNoLog(logs, /subresource-integrity/); }); }); }); diff --git a/packages/angular/build/src/builders/dev-server/builder.ts b/packages/angular/build/src/builders/dev-server/builder.ts index 4ea11d5f11e9..d75f999d84fd 100644 --- a/packages/angular/build/src/builders/dev-server/builder.ts +++ b/packages/angular/build/src/builders/dev-server/builder.ts @@ -18,7 +18,7 @@ import { import { normalizeOptions } from './options'; import type { DevServerBuilderOutput } from './output'; import type { Schema as DevServerBuilderOptions } from './schema'; -import { serveWithVite } from './vite-server'; +import { serveWithVite } from './vite'; /** * A Builder that executes a development server based on the provided browser target option. diff --git a/packages/angular/build/src/builders/dev-server/options.ts b/packages/angular/build/src/builders/dev-server/options.ts index 3e0f59319117..b6da278f2936 100644 --- a/packages/angular/build/src/builders/dev-server/options.ts +++ b/packages/angular/build/src/builders/dev-server/options.ts @@ -93,6 +93,7 @@ export async function normalizeOptions( poll, open, verbose, + define, watch, liveReload, hmr, @@ -114,6 +115,7 @@ export async function normalizeOptions( poll, open, verbose, + define, watch, liveReload: !!liveReload, hmr: hmr ?? !!liveReload, diff --git a/packages/angular/build/src/builders/dev-server/schema.json b/packages/angular/build/src/builders/dev-server/schema.json index 41902e43d8d0..023478ff7e52 100644 --- a/packages/angular/build/src/builders/dev-server/schema.json +++ b/packages/angular/build/src/builders/dev-server/schema.json @@ -53,6 +53,13 @@ } ] }, + "define": { + "description": "Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries. The value will be used directly. String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "headers": { "type": "object", "description": "Custom HTTP headers to be added to all responses.", 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 3bf4aa5fed6e..aa4763771fdd 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 @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import { logging } from '@angular-devkit/core'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO, expectLog, expectNoLog } from '../setup'; describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { describe('Behavior: "Rebuild Error Detection"', () => { @@ -31,21 +30,13 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT async ({ result, logs }) => { expect(result?.success).toBeFalse(); debugger; - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); + expectLog(logs, 'Unexpected character "EOF"'); await harness.appendToFile('src/app/app.component.html', '>'); }, ({ result, logs }) => { expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); + expectNoLog(logs, 'Unexpected character "EOF"'); }, ], { outputLogsOnFailure: false }, diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts index 083773529058..65f34bddf94d 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -/* eslint-disable import/no-extraneous-dependencies */ -import { tags } from '@angular-devkit/core'; import { createServer } from 'node:http'; import { createProxyServer } from 'http-proxy'; import { AddressInfo } from 'node:net'; @@ -43,58 +41,58 @@ async function createProxy(target: string, secure: boolean, ws = true): Promise< target, secure, ssl: secure && { - key: tags.stripIndents` - -----BEGIN RSA PRIVATE KEY----- - MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEBRUsUz4rdcMt - CQGLvG3SzUinsmgdgOyTNQNA0eOMyRSrmS8L+F/kSLUnqqu4mzdeqDzo2Xj553jK - dRqMCRFGJuGnQ/VIbW2A+ywgrqILuDyF5i4PL1aQW4yJ7TnXfONKfpswQArlN6DF - gBYJtoJlf8XD1sOeJpsv/O46/ix/wngQ+GwQQ2cfqxQT0fE9SBCY23VNt3SPUJ3k - 9etJMvJ9U9GHSb1CFdNQe7Gyx7xdKf1TazB27ElNZEg2aF99if47uRskYjvvFivy - 7nxGx/ccIwjwNMpk29AsKG++0sn1yTK7tD5Px6aCSVK0BKbdXZS2euJor8hASGBJ - 3GpVGJvdAgMBAAECggEAapYo8TVCdPdP7ckb4hPP0/R0MVu9aW2VNmZ5ImH+zar5 - ZmWhQ20HF2bBupP/VB5yeTIaDLNUKO9Iqy4KBWNY1UCHKyC023FFPgFV+V98FctU - faqwGOmwtEZToRwxe48ZOISndhEc247oCPyg/x8SwIY9z0OUkwaDFBEAqWtUXxM3 - /SPpCT5ilLgxnRgVB8Fj5Z0q7ThnxNVOmVC1OSIakEj46PzmMXn1pCKLOCUmAAOQ - BnrOZuty2b8b2M/GHsktLZwojQQJmArnIBymTXQTVhaGgKSyOv1qvHLp9L1OJf0/ - Xm+/TqT6ztzhzlftcObdfQZZ5JuoEwlvyrsGFlA3MQKBgQDiQC3KYMG8ViJkWrv6 - XNAFEoAjVEKrtirGWJ66YfQ9KSJ7Zttrd1Y1V1OLtq3z4YMH39wdQ8rOD+yR8mWV - 6Tnsxma6yJXAH8uan8iVbxjIZKF1hnvNCxUoxYmWOmTLcEQMzmxvTzAiR+s6R6Uj - 9LgGqppt30nM4wnOhOJU6UxqbwKBgQDdy03KidbPZuycJSy1C9AIt0jlrxDsYm+U - fZrB6mHEZcgoZS5GbLKinQCdGcgERa05BXvJmNbfZtT5a37YEnbjsTImIhDiBP5P - nW36/9a3Vg1svd1KP2206/Bh3gfZbgTsQg4YogXgjf0Uzuvw18btgTtLVpVyeuqz - TU3eeF30cwKBgQCN6lvOmapsDEs+T3uhqx4AUH53qp63PmjOSUAnANJGmsq6ROZV - HmHAy6nn9Qpf85BRHCXhZWiMoIhvc3As/EINNtWxS6hC/q6jqp4SvcD50cVFBroY - /16iWGXZCX+37A+DSOfTWgSDPEFcKRx41UOpStHbITgVgEPieo/NWxlHmQKBgQDX - JOLs2RB6V0ilnpnjdPXzvncD9fHgmwvJap24BPeZX3HtXViqD76oZsu1mNCg9EW3 - zk3pnEyyoDlvSIreZerVq4kN3HWsCVP3Pqr0kz9g0CRtmy8RWr28hjHDfXD3xPUZ - iGnMEz7IOHOKv722/liFAprV1cNaLUmFbDNg3jmlaQKBgQDG5WwngPhOHmjTnSml - amfEz9a4yEhQqpqgVNW5wwoXOf6DbjL2m/maJh01giThj7inMcbpkZlIclxD0Eu6 - Lof+ctCeqSAJvaVPmd+nv8Yp26zsF1yM8ax9xXjrIvv9fSbycNveGTDCsNNTiYoW - QyvMqmN1kGy20SZbQDD/fLfqBQ== - -----END RSA PRIVATE KEY----- + key: ` +-----BEGIN RSA PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEBRUsUz4rdcMt +CQGLvG3SzUinsmgdgOyTNQNA0eOMyRSrmS8L+F/kSLUnqqu4mzdeqDzo2Xj553jK +dRqMCRFGJuGnQ/VIbW2A+ywgrqILuDyF5i4PL1aQW4yJ7TnXfONKfpswQArlN6DF +gBYJtoJlf8XD1sOeJpsv/O46/ix/wngQ+GwQQ2cfqxQT0fE9SBCY23VNt3SPUJ3k +9etJMvJ9U9GHSb1CFdNQe7Gyx7xdKf1TazB27ElNZEg2aF99if47uRskYjvvFivy +7nxGx/ccIwjwNMpk29AsKG++0sn1yTK7tD5Px6aCSVK0BKbdXZS2euJor8hASGBJ +3GpVGJvdAgMBAAECggEAapYo8TVCdPdP7ckb4hPP0/R0MVu9aW2VNmZ5ImH+zar5 +ZmWhQ20HF2bBupP/VB5yeTIaDLNUKO9Iqy4KBWNY1UCHKyC023FFPgFV+V98FctU +faqwGOmwtEZToRwxe48ZOISndhEc247oCPyg/x8SwIY9z0OUkwaDFBEAqWtUXxM3 +/SPpCT5ilLgxnRgVB8Fj5Z0q7ThnxNVOmVC1OSIakEj46PzmMXn1pCKLOCUmAAOQ +BnrOZuty2b8b2M/GHsktLZwojQQJmArnIBymTXQTVhaGgKSyOv1qvHLp9L1OJf0/ +Xm+/TqT6ztzhzlftcObdfQZZ5JuoEwlvyrsGFlA3MQKBgQDiQC3KYMG8ViJkWrv6 +XNAFEoAjVEKrtirGWJ66YfQ9KSJ7Zttrd1Y1V1OLtq3z4YMH39wdQ8rOD+yR8mWV +6Tnsxma6yJXAH8uan8iVbxjIZKF1hnvNCxUoxYmWOmTLcEQMzmxvTzAiR+s6R6Uj +9LgGqppt30nM4wnOhOJU6UxqbwKBgQDdy03KidbPZuycJSy1C9AIt0jlrxDsYm+U +fZrB6mHEZcgoZS5GbLKinQCdGcgERa05BXvJmNbfZtT5a37YEnbjsTImIhDiBP5P +nW36/9a3Vg1svd1KP2206/Bh3gfZbgTsQg4YogXgjf0Uzuvw18btgTtLVpVyeuqz +TU3eeF30cwKBgQCN6lvOmapsDEs+T3uhqx4AUH53qp63PmjOSUAnANJGmsq6ROZV +HmHAy6nn9Qpf85BRHCXhZWiMoIhvc3As/EINNtWxS6hC/q6jqp4SvcD50cVFBroY +/16iWGXZCX+37A+DSOfTWgSDPEFcKRx41UOpStHbITgVgEPieo/NWxlHmQKBgQDX +JOLs2RB6V0ilnpnjdPXzvncD9fHgmwvJap24BPeZX3HtXViqD76oZsu1mNCg9EW3 +zk3pnEyyoDlvSIreZerVq4kN3HWsCVP3Pqr0kz9g0CRtmy8RWr28hjHDfXD3xPUZ +iGnMEz7IOHOKv722/liFAprV1cNaLUmFbDNg3jmlaQKBgQDG5WwngPhOHmjTnSml +amfEz9a4yEhQqpqgVNW5wwoXOf6DbjL2m/maJh01giThj7inMcbpkZlIclxD0Eu6 +Lof+ctCeqSAJvaVPmd+nv8Yp26zsF1yM8ax9xXjrIvv9fSbycNveGTDCsNNTiYoW +QyvMqmN1kGy20SZbQDD/fLfqBQ== +-----END RSA PRIVATE KEY----- `, - cert: tags.stripIndents` - -----BEGIN CERTIFICATE----- - MIIDXTCCAkWgAwIBAgIJALz8gD/gAt0OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV - BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX - aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMTgyMTQ5WhcNMTkxMDIzMTgyMTQ5WjBF - MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 - ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB - CgKCAQEAxAUVLFM+K3XDLQkBi7xt0s1Ip7JoHYDskzUDQNHjjMkUq5kvC/hf5Ei1 - J6qruJs3Xqg86Nl4+ed4ynUajAkRRibhp0P1SG1tgPssIK6iC7g8heYuDy9WkFuM - ie0513zjSn6bMEAK5TegxYAWCbaCZX/Fw9bDniabL/zuOv4sf8J4EPhsEENnH6sU - E9HxPUgQmNt1Tbd0j1Cd5PXrSTLyfVPRh0m9QhXTUHuxsse8XSn9U2swduxJTWRI - NmhffYn+O7kbJGI77xYr8u58Rsf3HCMI8DTKZNvQLChvvtLJ9ckyu7Q+T8emgklS - tASm3V2UtnriaK/IQEhgSdxqVRib3QIDAQABo1AwTjAdBgNVHQ4EFgQUDZBhVKdb - 3BRhLIhuuE522Vsul0IwHwYDVR0jBBgwFoAUDZBhVKdb3BRhLIhuuE522Vsul0Iw - DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABh9WWZwWLgb9/DcTxL72 - 6pI96t4jiF79Q+pPefkaIIi0mE6yodWrTAsBQu9I6bNRaEcCSoiXkP2bqskD/UGg - LwUFgSrDOAA3UjdHw3QU5g2NocduG7mcFwA40TB98sOsxsUyYlzSyWzoiQWwPYwb - hek1djuWkqPXsTjlj54PTPN/SjTFmo4p5Ip6nbRf2nOREl7v0rJpGbJvXiCMYyd+ - Zv+j4mRjCGo8ysMR2HjCUGkYReLAgKyyz3M7i8vevJhKslyOmy6Txn4F0nPVumaU - DDIy4xXPW1STWfsmSYJfYW3wa0wk+pJQ3j2cTzkPQQ8gwpvM3U9DJl43uwb37v6I - 7Q== - -----END CERTIFICATE----- + cert: ` +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJALz8gD/gAt0OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMTgyMTQ5WhcNMTkxMDIzMTgyMTQ5WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAxAUVLFM+K3XDLQkBi7xt0s1Ip7JoHYDskzUDQNHjjMkUq5kvC/hf5Ei1 +J6qruJs3Xqg86Nl4+ed4ynUajAkRRibhp0P1SG1tgPssIK6iC7g8heYuDy9WkFuM +ie0513zjSn6bMEAK5TegxYAWCbaCZX/Fw9bDniabL/zuOv4sf8J4EPhsEENnH6sU +E9HxPUgQmNt1Tbd0j1Cd5PXrSTLyfVPRh0m9QhXTUHuxsse8XSn9U2swduxJTWRI +NmhffYn+O7kbJGI77xYr8u58Rsf3HCMI8DTKZNvQLChvvtLJ9ckyu7Q+T8emgklS +tASm3V2UtnriaK/IQEhgSdxqVRib3QIDAQABo1AwTjAdBgNVHQ4EFgQUDZBhVKdb +3BRhLIhuuE522Vsul0IwHwYDVR0jBBgwFoAUDZBhVKdb3BRhLIhuuE522Vsul0Iw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABh9WWZwWLgb9/DcTxL72 +6pI96t4jiF79Q+pPefkaIIi0mE6yodWrTAsBQu9I6bNRaEcCSoiXkP2bqskD/UGg +LwUFgSrDOAA3UjdHw3QU5g2NocduG7mcFwA40TB98sOsxsUyYlzSyWzoiQWwPYwb +hek1djuWkqPXsTjlj54PTPN/SjTFmo4p5Ip6nbRf2nOREl7v0rJpGbJvXiCMYyd+ +Zv+j4mRjCGo8ysMR2HjCUGkYReLAgKyyz3M7i8vevJhKslyOmy6Txn4F0nPVumaU +DDIy4xXPW1STWfsmSYJfYW3wa0wk+pJQ3j2cTzkPQQ8gwpvM3U9DJl43uwb37v6I +7Q== +-----END CERTIFICATE----- `, }, }).listen(proxyPort); diff --git a/packages/angular/build/src/builders/dev-server/tests/options/define_spec.ts b/packages/angular/build/src/builders/dev-server/tests/options/define_spec.ts new file mode 100644 index 000000000000..3c6ea08e15b4 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/tests/options/define_spec.ts @@ -0,0 +1,92 @@ +/** + * @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 { executeDevServer } from '../../index'; +import { executeOnceAndFetch } from '../execute-fetch'; +import { describeServeBuilder } from '../jasmine-helpers'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; + +describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { + describe('option: "define"', () => { + beforeEach(() => { + setupTarget(harness); + + // Application code + harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + console.log(TEST); + // @ts-ignore + console.log(BUILD); + // @ts-ignore + console.log(SERVE); + `, + ); + }); + + it('should replace global identifiers in the application', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + define: { + TEST: JSON.stringify('test123'), + }, + }); + + const { result, response } = await executeOnceAndFetch(harness, '/main.js'); + + expect(result?.success).toBeTrue(); + const content = await response?.text(); + expect(content).toContain('console.log("test123")'); + }); + + it('should merge "define" option from dev-server and build', async () => { + harness.modifyTarget('build', (options) => { + options.define = { + BUILD: JSON.stringify('build'), + }; + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + define: { + SERVE: JSON.stringify('serve'), + }, + }); + + const { result, response } = await executeOnceAndFetch(harness, '/main.js'); + + expect(result?.success).toBeTrue(); + const content = await response?.text(); + expect(content).toContain('console.log("build")'); + expect(content).toContain('console.log("serve")'); + }); + + it('should overwrite "define" option from build with the one from dev-server', async () => { + harness.modifyTarget('build', (options) => { + options.define = { + TEST: JSON.stringify('build'), + }; + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + define: { + TEST: JSON.stringify('serve'), + }, + }); + + const { result, response } = await executeOnceAndFetch(harness, '/main.js'); + + expect(result?.success).toBeTrue(); + const content = await response?.text(); + expect(content).toContain('console.log("serve")'); + expect(content).not.toContain('console.log("build")'); + }); + }); +}); diff --git a/packages/angular/build/src/builders/dev-server/vite/hmr.ts b/packages/angular/build/src/builders/dev-server/vite/hmr.ts new file mode 100644 index 000000000000..ae2fb1b91bb0 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/vite/hmr.ts @@ -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 type { ɵdestroyAngularServerApp as destroyAngularServerApp } from '@angular/ssr'; +import type { BuilderContext } from '@angular-devkit/architect'; +import { join } from 'node:path'; +import type { ViteDevServer } from 'vite'; +import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; +import { BuildOutputFileType } from '../internal'; +import type { NormalizedDevServerOptions } from '../options'; +import type { OutputAssetRecord, OutputFileRecord } from './utils'; + +/** + * Invalidates any updated asset or generated files and resets their `updated` state. + * This function also clears the server application cache when necessary. + * + * @returns A list of files that were updated and invalidated. + */ +export async function invalidateUpdatedFiles( + normalizePath: (id: string) => string, + generatedFiles: Map, + assetFiles: Map, + server: ViteDevServer, +): Promise { + const updatedFiles: string[] = []; + + // Invalidate any updated asset + for (const [file, record] of assetFiles) { + if (!record.updated) { + continue; + } + + record.updated = false; + updatedFiles.push(file); + } + + // Invalidate any updated files + let serverApplicationChanged = false; + for (const [file, record] of generatedFiles) { + if (!record.updated) { + continue; + } + + record.updated = false; + updatedFiles.push(file); + serverApplicationChanged ||= record.type === BuildOutputFileType.ServerApplication; + + const updatedModules = server.moduleGraph.getModulesByFile( + normalizePath(join(server.config.root, file)), + ); + updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m)); + } + + if (serverApplicationChanged) { + // Clear the server app cache and + // trigger module evaluation before reload to initiate dependency optimization. + const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as { + ɵdestroyAngularServerApp: typeof destroyAngularServerApp; + }; + + ɵdestroyAngularServerApp(); + } + + return updatedFiles; +} + +/** + * Handles updates for the client by sending HMR or full page reload commands + * based on the updated files. It also ensures proper tracking of component styles and determines if + * a full reload is needed. + */ +export function handleUpdate( + server: ViteDevServer, + serverOptions: NormalizedDevServerOptions, + logger: BuilderContext['logger'], + componentStyles: Map, + updatedFiles: string[], +): void { + if (!updatedFiles.length) { + return; + } + + if (serverOptions.hmr) { + if (updatedFiles.every((f) => f.endsWith('.css'))) { + let requiresReload = false; + const timestamp = Date.now(); + const updates = updatedFiles.flatMap((filePath) => { + // For component styles, an HMR update must be sent for each one with the corresponding + // component identifier search parameter (`ngcomp`). The Vite client code will not keep + // the existing search parameters when it performs an update and each one must be + // specified explicitly. Typically, there is only one each though as specific style files + // are not typically reused across components. + const record = componentStyles.get(filePath); + if (record) { + if (record.reload) { + // Shadow DOM components currently require a full reload. + // Vite's CSS hot replacement does not support shadow root searching. + requiresReload = true; + + return []; + } + + return Array.from(record.used ?? []).map((id) => { + return { + type: 'css-update' as const, + timestamp, + path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''), + acceptedPath: filePath, + }; + }); + } + + return { + type: 'css-update' as const, + timestamp, + path: filePath, + acceptedPath: filePath, + }; + }); + + if (!requiresReload) { + server.ws.send({ + type: 'update', + updates, + }); + logger.info('Stylesheet update sent to client(s).'); + + return; + } + } + } + + // Send reload command to clients + if (serverOptions.liveReload) { + // Clear used component tracking on full reload + componentStyles.forEach((record) => record.used?.clear()); + + server.ws.send({ + type: 'full-reload', + path: '*', + }); + + logger.info('Page reload sent to client(s).'); + } +} diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite/index.ts similarity index 52% rename from packages/angular/build/src/builders/dev-server/vite-server.ts rename to packages/angular/build/src/builders/dev-server/vite/index.ts index 6f68a37691c6..b1f70379125c 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite/index.ts @@ -6,59 +6,38 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { ɵdestroyAngularServerApp as destroyAngularServerApp } from '@angular/ssr'; import type { BuilderContext } from '@angular-devkit/architect'; import type { Plugin } from 'esbuild'; import assert from 'node:assert'; -import { readFile } from 'node:fs/promises'; import { builtinModules, isBuiltin } from 'node:module'; import { join } from 'node:path'; -import type { Connect, InlineConfig, ViteDevServer } from 'vite'; -import type { ComponentStyleRecord } from '../../tools/vite/middlewares'; -import { - ServerSsrMode, - createAngularLocaleDataPlugin, - createAngularMemoryPlugin, - createAngularSetupMiddlewaresPlugin, - createAngularSsrTransformPlugin, - createRemoveIdPrefixPlugin, -} from '../../tools/vite/plugins'; -import { EsbuildLoaderOption, getDepOptimizationConfig } from '../../tools/vite/utils'; -import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils'; -import { useComponentStyleHmr, useComponentTemplateHmr } from '../../utils/environment-options'; -import { loadEsmModule } from '../../utils/load-esm'; -import { Result, ResultFile, ResultKind } from '../application/results'; -import { OutputHashing } from '../application/schema'; +import type { Connect, ViteDevServer } from 'vite'; +import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; +import { ServerSsrMode } from '../../../tools/vite/plugins'; +import { EsbuildLoaderOption } from '../../../tools/vite/utils'; +import { normalizeSourceMaps } from '../../../utils'; +import { useComponentStyleHmr, useComponentTemplateHmr } from '../../../utils/environment-options'; +import { Result, ResultKind } from '../../application/results'; +import { OutputHashing } from '../../application/schema'; import { type ApplicationBuilderInternalOptions, - BuildOutputFileType, type ExternalResultMetadata, JavaScriptTransformer, getSupportedBrowsers, isZonelessApp, transformSupportedBrowsersToTargets, -} from './internal'; -import type { NormalizedDevServerOptions } from './options'; -import type { DevServerBuilderOutput } from './output'; - -interface OutputFileRecord { - contents: Uint8Array; - size: number; - hash: string; - updated: boolean; - servable: boolean; - type: BuildOutputFileType; -} - -interface OutputAssetRecord { - source: string; - updated: boolean; -} - -interface DevServerExternalResultMetadata extends Omit { - explicitBrowser: string[]; - explicitServer: string[]; -} +} from '../internal'; +import type { NormalizedDevServerOptions } from '../options'; +import type { DevServerBuilderOutput } from '../output'; +import { handleUpdate, invalidateUpdatedFiles } from './hmr'; +import { setupServer } from './server'; +import { + DevServerExternalResultMetadata, + OutputAssetRecord, + OutputFileRecord, + isAbsoluteUrl, + updateResultRecord, +} from './utils'; export type BuilderAction = ( options: ApplicationBuilderInternalOptions, @@ -70,7 +49,7 @@ export type BuilderAction = ( * Build options that are also present on the dev server but are only passed * to the build. */ -const CONVENIENCE_BUILD_OPTIONS = ['watch', 'poll', 'verbose'] as const; +const CONVENIENCE_BUILD_OPTIONS = ['watch', 'poll', 'verbose', 'define'] as const; // eslint-disable-next-line max-lines-per-function export async function* serveWithVite( @@ -96,7 +75,15 @@ export async function* serveWithVite( for (const optionName of CONVENIENCE_BUILD_OPTIONS) { const optionValue = serverOptions[optionName]; if (optionValue !== undefined) { - rawBrowserOptions[optionName] = optionValue; + if (optionName === 'define' && rawBrowserOptions[optionName]) { + // Define has merging behavior within the application + for (const [key, value] of Object.entries(optionValue)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (rawBrowserOptions[optionName] as any)[key] = value; + } + } else { + rawBrowserOptions[optionName] = optionValue; + } } } @@ -195,8 +182,7 @@ export async function* serveWithVite( // The index HTML path will be updated from the build results if provided by the builder let htmlIndexPath = 'index.html'; - // dynamically import Vite for ESM compatibility - const { createServer, normalizePath } = await loadEsmModule('vite'); + const { createServer, normalizePath } = await import('vite'); let server: ViteDevServer | undefined; let serverUrl: URL | undefined; @@ -456,6 +442,7 @@ export async function* serveWithVite( browserOptions.loader as EsbuildLoaderOption | undefined, { ...browserOptions.define, + 'ngJitMode': browserOptions.aot ? 'false' : 'true', 'ngHmrMode': browserOptions.templateUpdates ? 'true' : 'false', }, extensions?.middleware, @@ -555,425 +542,3 @@ export async function* serveWithVite( await new Promise((resolve) => (deferred = resolve)); } - -/** - * Invalidates any updated asset or generated files and resets their `updated` state. - * This function also clears the server application cache when necessary. - * - * @returns A list of files that were updated and invalidated. - */ -async function invalidateUpdatedFiles( - normalizePath: (id: string) => string, - generatedFiles: Map, - assetFiles: Map, - server: ViteDevServer, -): Promise { - const updatedFiles: string[] = []; - - // Invalidate any updated asset - for (const [file, record] of assetFiles) { - if (!record.updated) { - continue; - } - - record.updated = false; - updatedFiles.push(file); - } - - // Invalidate any updated files - let serverApplicationChanged = false; - for (const [file, record] of generatedFiles) { - if (!record.updated) { - continue; - } - - record.updated = false; - updatedFiles.push(file); - serverApplicationChanged ||= record.type === BuildOutputFileType.ServerApplication; - - const updatedModules = server.moduleGraph.getModulesByFile( - normalizePath(join(server.config.root, file)), - ); - updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m)); - } - - if (serverApplicationChanged) { - // Clear the server app cache and - // trigger module evaluation before reload to initiate dependency optimization. - const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as { - ɵdestroyAngularServerApp: typeof destroyAngularServerApp; - }; - - ɵdestroyAngularServerApp(); - } - - return updatedFiles; -} - -/** - * Handles updates for the client by sending HMR or full page reload commands - * based on the updated files. It also ensures proper tracking of component styles and determines if - * a full reload is needed. - */ -function handleUpdate( - server: ViteDevServer, - serverOptions: NormalizedDevServerOptions, - logger: BuilderContext['logger'], - componentStyles: Map, - updatedFiles: string[], -): void { - if (!updatedFiles.length) { - return; - } - - if (serverOptions.hmr) { - if (updatedFiles.every((f) => f.endsWith('.css'))) { - let requiresReload = false; - const timestamp = Date.now(); - const updates = updatedFiles.flatMap((filePath) => { - // For component styles, an HMR update must be sent for each one with the corresponding - // component identifier search parameter (`ngcomp`). The Vite client code will not keep - // the existing search parameters when it performs an update and each one must be - // specified explicitly. Typically, there is only one each though as specific style files - // are not typically reused across components. - const record = componentStyles.get(filePath); - if (record) { - if (record.reload) { - // Shadow DOM components currently require a full reload. - // Vite's CSS hot replacement does not support shadow root searching. - requiresReload = true; - - return []; - } - - return Array.from(record.used ?? []).map((id) => { - return { - type: 'css-update' as const, - timestamp, - path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''), - acceptedPath: filePath, - }; - }); - } - - return { - type: 'css-update' as const, - timestamp, - path: filePath, - acceptedPath: filePath, - }; - }); - - if (!requiresReload) { - server.ws.send({ - type: 'update', - updates, - }); - logger.info('Stylesheet update sent to client(s).'); - - return; - } - } - } - - // Send reload command to clients - if (serverOptions.liveReload) { - // Clear used component tracking on full reload - componentStyles.forEach((record) => record.used?.clear()); - - server.ws.send({ - type: 'full-reload', - path: '*', - }); - - logger.info('Page reload sent to client(s).'); - } -} - -function updateResultRecord( - outputPath: string, - file: ResultFile, - normalizePath: (id: string) => string, - htmlIndexPath: string, - generatedFiles: Map, - assetFiles: Map, - componentStyles: Map, - initial = false, -): void { - if (file.origin === 'disk') { - assetFiles.set('/' + normalizePath(outputPath), { - source: normalizePath(file.inputPath), - updated: !initial, - }); - - return; - } - - let filePath; - if (outputPath === htmlIndexPath) { - // Convert custom index output path to standard index path for dev-server usage. - // This mimics the Webpack dev-server behavior. - filePath = '/index.html'; - } else { - filePath = '/' + normalizePath(outputPath); - } - - const servable = - file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media; - - // Skip analysis of sourcemaps - if (filePath.endsWith('.map')) { - generatedFiles.set(filePath, { - contents: file.contents, - servable, - size: file.contents.byteLength, - hash: file.hash, - type: file.type, - updated: false, - }); - - return; - } - - // New or updated file - generatedFiles.set(filePath, { - contents: file.contents, - size: file.contents.byteLength, - hash: file.hash, - // Consider the files updated except on the initial build result - updated: !initial, - type: file.type, - servable, - }); - - // Record any external component styles - if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) { - const componentStyle = componentStyles.get(filePath); - if (componentStyle) { - componentStyle.rawContent = file.contents; - } else { - componentStyles.set(filePath, { - rawContent: file.contents, - }); - } - } -} - -// eslint-disable-next-line max-lines-per-function -export async function setupServer( - serverOptions: NormalizedDevServerOptions, - outputFiles: Map, - assets: Map, - preserveSymlinks: boolean | undefined, - externalMetadata: DevServerExternalResultMetadata, - ssrMode: ServerSsrMode, - prebundleTransformer: JavaScriptTransformer, - target: string[], - zoneless: boolean, - componentStyles: Map, - templateUpdates: Map, - prebundleLoaderExtensions: EsbuildLoaderOption | undefined, - define: ApplicationBuilderInternalOptions['define'], - extensionMiddleware?: Connect.NextHandleFunction[], - indexHtmlTransformer?: (content: string) => Promise, - thirdPartySourcemaps = false, -): Promise { - const proxy = await loadProxyConfiguration( - serverOptions.workspaceRoot, - serverOptions.proxyConfig, - ); - - // dynamically import Vite for ESM compatibility - const { normalizePath } = await loadEsmModule('vite'); - - // Path will not exist on disk and only used to provide separate path for Vite requests - const virtualProjectRoot = normalizePath( - join(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project), - ); - - // Files used for SSR warmup. - let ssrFiles: string[] | undefined; - switch (ssrMode) { - case ServerSsrMode.InternalSsrMiddleware: - ssrFiles = ['./main.server.mjs']; - break; - case ServerSsrMode.ExternalSsrMiddleware: - ssrFiles = ['./main.server.mjs', './server.mjs']; - break; - } - - /** - * Required when using `externalDependencies` to prevent Vite load errors. - * - * @note Can be removed if Vite introduces native support for externals. - * @note Vite misresolves browser modules in SSR when accessing URLs with multiple segments - * (e.g., 'foo/bar'), as they are not correctly re-based from the base href. - */ - const preTransformRequests = - externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr; - const cacheDir = join(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite'); - const configuration: InlineConfig = { - configFile: false, - envFile: false, - cacheDir, - root: virtualProjectRoot, - publicDir: false, - esbuild: false, - mode: 'development', - // We use custom as we do not rely on Vite's htmlFallbackMiddleware and indexHtmlMiddleware. - appType: 'custom', - css: { - devSourcemap: true, - }, - // Ensure custom 'file' loader build option entries are handled by Vite in application code that - // reference third-party libraries. Relative usage is handled directly by the build and not Vite. - // Only 'file' loader entries are currently supported directly by Vite. - assetsInclude: - prebundleLoaderExtensions && - Object.entries(prebundleLoaderExtensions) - .filter(([, value]) => value === 'file') - // Create a file extension glob for each key - .map(([key]) => '*' + key), - // Vite will normalize the `base` option by adding a leading slash. - base: serverOptions.servePath, - resolve: { - mainFields: ['es2020', 'browser', 'module', 'main'], - preserveSymlinks, - }, - dev: { - preTransformRequests, - }, - server: { - preTransformRequests, - warmup: { - ssrFiles, - }, - port: serverOptions.port, - strictPort: true, - host: serverOptions.host, - open: serverOptions.open, - allowedHosts: serverOptions.allowedHosts, - headers: serverOptions.headers, - // Disable the websocket if live reload is disabled (false/undefined are the only valid values) - ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined, - // When server-side rendering (SSR) is enabled togather with SSL and Express is being used, - // we must configure Vite to use HTTP/1.1. - // This is necessary because Express does not support HTTP/2. - // We achieve this by defining an empty proxy. - // See: https://github.com/vitejs/vite/blob/c4b532cc900bf988073583511f57bd581755d5e3/packages/vite/src/node/http.ts#L106 - proxy: - serverOptions.ssl && ssrMode === ServerSsrMode.ExternalSsrMiddleware - ? (proxy ?? {}) - : proxy, - cors: { - // This will add the header `Access-Control-Allow-Origin: http://example.com`, - // where `http://example.com` is the requesting origin. - origin: true, - // Allow preflight requests to be proxied. - preflightContinue: true, - }, - // File watching is handled by the build directly. `null` disables file watching for Vite. - watch: null, - fs: { - // Ensure cache directory, node modules, and all assets are accessible by the client. - // The first two are required for Vite to function in prebundling mode (the default) and to load - // the Vite client-side code for browser reloading. These would be available by default but when - // the `allow` option is explicitly configured, they must be included manually. - allow: [ - cacheDir, - join(serverOptions.workspaceRoot, 'node_modules'), - ...[...assets.values()].map(({ source }) => source), - ], - }, - }, - ssr: { - // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. - noExternal: /.*/, - // Exclude any Node.js built in module and provided dependencies (currently build defined externals) - external: externalMetadata.explicitServer, - optimizeDeps: getDepOptimizationConfig({ - // Only enable with caching since it causes prebundle dependencies to be cached - disabled: serverOptions.prebundle === false, - // Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins) - exclude: externalMetadata.explicitServer, - // Include all implict dependencies from the external packages internal option - include: externalMetadata.implicitServer, - ssr: true, - prebundleTransformer, - zoneless, - target, - loader: prebundleLoaderExtensions, - thirdPartySourcemaps, - define, - }), - }, - plugins: [ - createAngularLocaleDataPlugin(), - createAngularSetupMiddlewaresPlugin({ - outputFiles, - assets, - indexHtmlTransformer, - extensionMiddleware, - componentStyles, - templateUpdates, - ssrMode, - resetComponentUpdates: () => templateUpdates.clear(), - projectRoot: serverOptions.projectRoot, - }), - createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser), - await createAngularSsrTransformPlugin(serverOptions.workspaceRoot), - await createAngularMemoryPlugin({ - virtualProjectRoot, - outputFiles, - templateUpdates, - external: externalMetadata.explicitBrowser, - disableViteTransport: !serverOptions.liveReload, - }), - ], - // Browser only optimizeDeps. (This does not run for SSR dependencies). - optimizeDeps: getDepOptimizationConfig({ - // Only enable with caching since it causes prebundle dependencies to be cached - disabled: serverOptions.prebundle === false, - // Exclude any explicitly defined dependencies (currently build defined externals) - exclude: externalMetadata.explicitBrowser, - // Include all implict dependencies from the external packages internal option - include: externalMetadata.implicitBrowser, - ssr: false, - prebundleTransformer, - target, - zoneless, - loader: prebundleLoaderExtensions, - thirdPartySourcemaps, - define, - }), - }; - - if (serverOptions.ssl) { - if (serverOptions.sslCert && serverOptions.sslKey) { - configuration.server ??= {}; - // server configuration is defined above - configuration.server.https = { - cert: await readFile(serverOptions.sslCert), - key: await readFile(serverOptions.sslKey), - }; - } else { - const { default: basicSslPlugin } = await import('@vitejs/plugin-basic-ssl'); - configuration.plugins ??= []; - configuration.plugins.push(basicSslPlugin()); - } - } - - return configuration; -} - -/** - * Checks if the given value is an absolute URL. - * - * This function helps in avoiding Vite's prebundling from processing absolute URLs (http://, https://, //) as files. - * - * @param value - The URL or path to check. - * @returns `true` if the value is not an absolute URL; otherwise, `false`. - */ -function isAbsoluteUrl(value: string): boolean { - return /^(?:https?:)?\/\//.test(value); -} diff --git a/packages/angular/build/src/builders/dev-server/vite/server.ts b/packages/angular/build/src/builders/dev-server/vite/server.ts new file mode 100644 index 000000000000..9cabeabccfec --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/vite/server.ts @@ -0,0 +1,277 @@ +/** + * @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 { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { Connect, InlineConfig, SSROptions, ServerOptions } from 'vite'; +import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; +import { + ServerSsrMode, + createAngularMemoryPlugin, + createAngularServerSideSSLPlugin, + createAngularSetupMiddlewaresPlugin, + createAngularSsrTransformPlugin, + createRemoveIdPrefixPlugin, +} from '../../../tools/vite/plugins'; +import { EsbuildLoaderOption, getDepOptimizationConfig } from '../../../tools/vite/utils'; +import { loadProxyConfiguration } from '../../../utils'; +import { type ApplicationBuilderInternalOptions, JavaScriptTransformer } from '../internal'; +import type { NormalizedDevServerOptions } from '../options'; +import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord } from './utils'; + +async function createServerConfig( + serverOptions: NormalizedDevServerOptions, + assets: Map, + ssrMode: ServerSsrMode, + preTransformRequests: boolean, + cacheDir: string, +): Promise { + const proxy = await loadProxyConfiguration( + serverOptions.workspaceRoot, + serverOptions.proxyConfig, + ); + + // Files used for SSR warmup. + let ssrFiles: string[] | undefined; + switch (ssrMode) { + case ServerSsrMode.InternalSsrMiddleware: + ssrFiles = ['./main.server.mjs']; + break; + case ServerSsrMode.ExternalSsrMiddleware: + ssrFiles = ['./main.server.mjs', './server.mjs']; + break; + } + + const server: ServerOptions = { + preTransformRequests, + warmup: { + ssrFiles, + }, + port: serverOptions.port, + strictPort: true, + host: serverOptions.host, + open: serverOptions.open, + allowedHosts: serverOptions.allowedHosts, + headers: serverOptions.headers, + // Disable the websocket if live reload is disabled (false/undefined are the only valid values) + ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined, + // When server-side rendering (SSR) is enabled togather with SSL and Express is being used, + // we must configure Vite to use HTTP/1.1. + // This is necessary because Express does not support HTTP/2. + // We achieve this by defining an empty proxy. + // See: https://github.com/vitejs/vite/blob/c4b532cc900bf988073583511f57bd581755d5e3/packages/vite/src/node/http.ts#L106 + proxy: + serverOptions.ssl && ssrMode === ServerSsrMode.ExternalSsrMiddleware ? (proxy ?? {}) : proxy, + cors: { + // This will add the header `Access-Control-Allow-Origin: http://example.com`, + // where `http://example.com` is the requesting origin. + origin: true, + // Allow preflight requests to be proxied. + preflightContinue: true, + }, + // File watching is handled by the build directly. `null` disables file watching for Vite. + watch: null, + fs: { + // Ensure cache directory, node modules, and all assets are accessible by the client. + // The first two are required for Vite to function in prebundling mode (the default) and to load + // the Vite client-side code for browser reloading. These would be available by default but when + // the `allow` option is explicitly configured, they must be included manually. + allow: [ + cacheDir, + join(serverOptions.workspaceRoot, 'node_modules'), + ...[...assets.values()].map(({ source }) => source), + ], + }, + }; + + if (serverOptions.ssl) { + if (serverOptions.sslCert && serverOptions.sslKey) { + server.https = { + cert: await readFile(serverOptions.sslCert), + key: await readFile(serverOptions.sslKey), + }; + } + } + + return server; +} + +function createSsrConfig( + externalMetadata: DevServerExternalResultMetadata, + serverOptions: NormalizedDevServerOptions, + prebundleTransformer: JavaScriptTransformer, + zoneless: boolean, + target: string[], + prebundleLoaderExtensions: EsbuildLoaderOption | undefined, + thirdPartySourcemaps: boolean, + define: ApplicationBuilderInternalOptions['define'], +): SSROptions { + return { + // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. + noExternal: /.*/, + // Exclude any Node.js built in module and provided dependencies (currently build defined externals) + external: externalMetadata.explicitServer, + optimizeDeps: getDepOptimizationConfig({ + // Only enable with caching since it causes prebundle dependencies to be cached + disabled: serverOptions.prebundle === false, + // Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins) + exclude: externalMetadata.explicitServer, + // Include all implict dependencies from the external packages internal option + include: externalMetadata.implicitServer, + ssr: true, + prebundleTransformer, + zoneless, + target, + loader: prebundleLoaderExtensions, + thirdPartySourcemaps, + define, + }), + }; +} + +export async function setupServer( + serverOptions: NormalizedDevServerOptions, + outputFiles: Map, + assets: Map, + preserveSymlinks: boolean | undefined, + externalMetadata: DevServerExternalResultMetadata, + ssrMode: ServerSsrMode, + prebundleTransformer: JavaScriptTransformer, + target: string[], + zoneless: boolean, + componentStyles: Map, + templateUpdates: Map, + prebundleLoaderExtensions: EsbuildLoaderOption | undefined, + define: ApplicationBuilderInternalOptions['define'], + extensionMiddleware?: Connect.NextHandleFunction[], + indexHtmlTransformer?: (content: string) => Promise, + thirdPartySourcemaps = false, +): Promise { + const { normalizePath } = await import('vite'); + + // Path will not exist on disk and only used to provide separate path for Vite requests + const virtualProjectRoot = normalizePath( + join(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project), + ); + + /** + * Required when using `externalDependencies` to prevent Vite load errors. + * + * @note Can be removed if Vite introduces native support for externals. + * @note Vite misresolves browser modules in SSR when accessing URLs with multiple segments + * (e.g., 'foo/bar'), as they are not correctly re-based from the base href. + */ + const preTransformRequests = + externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr; + const cacheDir = join(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite'); + + const configuration: InlineConfig = { + configFile: false, + envFile: false, + cacheDir, + root: virtualProjectRoot, + publicDir: false, + esbuild: false, + mode: 'development', + // We use custom as we do not rely on Vite's htmlFallbackMiddleware and indexHtmlMiddleware. + appType: 'custom', + css: { + devSourcemap: true, + }, + // Ensure custom 'file' loader build option entries are handled by Vite in application code that + // reference third-party libraries. Relative usage is handled directly by the build and not Vite. + // Only 'file' loader entries are currently supported directly by Vite. + assetsInclude: + prebundleLoaderExtensions && + Object.entries(prebundleLoaderExtensions) + .filter(([, value]) => value === 'file') + // Create a file extension glob for each key + .map(([key]) => '*' + key), + // Vite will normalize the `base` option by adding a leading slash. + base: serverOptions.servePath, + resolve: { + mainFields: ['es2020', 'browser', 'module', 'main'], + preserveSymlinks, + }, + dev: { + preTransformRequests, + }, + server: await createServerConfig( + serverOptions, + assets, + ssrMode, + preTransformRequests, + cacheDir, + ), + ssr: + ssrMode === ServerSsrMode.NoSsr + ? undefined + : createSsrConfig( + externalMetadata, + serverOptions, + prebundleTransformer, + zoneless, + target, + prebundleLoaderExtensions, + thirdPartySourcemaps, + define, + ), + plugins: [ + createAngularSetupMiddlewaresPlugin({ + outputFiles, + assets, + indexHtmlTransformer, + extensionMiddleware, + componentStyles, + templateUpdates, + ssrMode, + resetComponentUpdates: () => templateUpdates.clear(), + projectRoot: serverOptions.projectRoot, + }), + createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser), + await createAngularSsrTransformPlugin(serverOptions.workspaceRoot), + await createAngularMemoryPlugin({ + virtualProjectRoot, + outputFiles, + templateUpdates, + external: externalMetadata.explicitBrowser, + disableViteTransport: !serverOptions.liveReload, + }), + ], + // Browser only optimizeDeps. (This does not run for SSR dependencies). + optimizeDeps: getDepOptimizationConfig({ + // Only enable with caching since it causes prebundle dependencies to be cached + disabled: serverOptions.prebundle === false, + // Exclude any explicitly defined dependencies (currently build defined externals) + exclude: externalMetadata.explicitBrowser, + // Include all implict dependencies from the external packages internal option + include: externalMetadata.implicitBrowser, + ssr: false, + prebundleTransformer, + target, + zoneless, + loader: prebundleLoaderExtensions, + thirdPartySourcemaps, + define, + }), + }; + + if (serverOptions.ssl) { + configuration.plugins ??= []; + if (!serverOptions.sslCert || !serverOptions.sslKey) { + const { default: basicSslPlugin } = await import('@vitejs/plugin-basic-ssl'); + configuration.plugins.push(basicSslPlugin()); + } + + if (ssrMode !== ServerSsrMode.NoSsr) { + configuration.plugins?.push(createAngularServerSideSSLPlugin()); + } + } + + return configuration; +} diff --git a/packages/angular/build/src/builders/dev-server/vite/utils.ts b/packages/angular/build/src/builders/dev-server/vite/utils.ts new file mode 100644 index 000000000000..e1e6b4f96847 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/vite/utils.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; +import type { ResultFile } from '../../application/results'; +import { BuildOutputFileType, type ExternalResultMetadata } from '../internal'; + +export interface OutputFileRecord { + contents: Uint8Array; + size: number; + hash: string; + updated: boolean; + servable: boolean; + type: BuildOutputFileType; +} + +export interface OutputAssetRecord { + source: string; + updated: boolean; +} + +export interface DevServerExternalResultMetadata extends Omit { + explicitBrowser: string[]; + explicitServer: string[]; +} + +export function updateResultRecord( + outputPath: string, + file: ResultFile, + normalizePath: (id: string) => string, + htmlIndexPath: string, + generatedFiles: Map, + assetFiles: Map, + componentStyles: Map, + initial = false, +): void { + if (file.origin === 'disk') { + assetFiles.set('/' + normalizePath(outputPath), { + source: normalizePath(file.inputPath), + updated: !initial, + }); + + return; + } + + let filePath; + if (outputPath === htmlIndexPath) { + // Convert custom index output path to standard index path for dev-server usage. + // This mimics the Webpack dev-server behavior. + filePath = '/index.html'; + } else { + filePath = '/' + normalizePath(outputPath); + } + + const servable = + file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media; + + // Skip analysis of sourcemaps + if (filePath.endsWith('.map')) { + generatedFiles.set(filePath, { + contents: file.contents, + servable, + size: file.contents.byteLength, + hash: file.hash, + type: file.type, + updated: false, + }); + + return; + } + + // New or updated file + generatedFiles.set(filePath, { + contents: file.contents, + size: file.contents.byteLength, + hash: file.hash, + // Consider the files updated except on the initial build result + updated: !initial, + type: file.type, + servable, + }); + + // Record any external component styles + if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) { + const componentStyle = componentStyles.get(filePath); + if (componentStyle) { + componentStyle.rawContent = file.contents; + } else { + componentStyles.set(filePath, { + rawContent: file.contents, + }); + } + } +} + +/** + * Checks if the given value is an absolute URL. + * + * This function helps in avoiding Vite's prebundling from processing absolute URLs (http://, https://, //) as files. + * + * @param value - The URL or path to check. + * @returns `true` if the value is not an absolute URL; otherwise, `false`. + */ +export function isAbsoluteUrl(value: string): boolean { + return /^(?:https?:)?\/\//.test(value); +} diff --git a/packages/angular/build/src/builders/extract-i18n/builder.ts b/packages/angular/build/src/builders/extract-i18n/builder.ts index 15b0156f749b..f74d1d219712 100644 --- a/packages/angular/build/src/builders/extract-i18n/builder.ts +++ b/packages/angular/build/src/builders/extract-i18n/builder.ts @@ -10,7 +10,6 @@ import type { Diagnostics } from '@angular/localize/tools'; import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import fs from 'node:fs'; import path from 'node:path'; -import { loadEsmModule } from '../../utils/load-esm'; import { assertCompatibleAngularVersion } from '../../utils/version'; import type { ApplicationBuilderExtensions } from '../application/options'; import { normalizeOptions } from './options'; @@ -51,8 +50,7 @@ export async function execute( // The package is a peer dependency and might not be present let localizeToolsModule; try { - localizeToolsModule = - await loadEsmModule('@angular/localize/tools'); + localizeToolsModule = await import('@angular/localize/tools'); } catch { return { success: false, diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index ae238b7239cf..34e94b1b7645 100644 --- a/packages/angular/build/src/builders/karma/application_builder.ts +++ b/packages/angular/build/src/builders/karma/application_builder.ts @@ -9,28 +9,32 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from 'karma'; import { randomUUID } from 'node:crypto'; +import { rmSync } from 'node:fs'; import * as fs from 'node:fs/promises'; -import type { IncomingMessage, ServerResponse } from 'node:http'; -import { createRequire } from 'node:module'; import path from 'node:path'; -import { ReadableStreamController } from 'node:stream/web'; -import { globSync } from 'tinyglobby'; -import { BuildOutputFileType } from '../../tools/esbuild/bundler-context'; -import { emitFilesToDisk } from '../../tools/esbuild/utils'; +import { ReadableStream } from 'node:stream/web'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; -import { getProjectRootPaths } from '../../utils/project-metadata'; +import { writeTestFiles } from '../../utils/test-files'; import { buildApplicationInternal } from '../application/index'; import { ApplicationBuilderInternalOptions } from '../application/options'; -import { Result, ResultFile, ResultKind } from '../application/results'; +import { Result, ResultKind } from '../application/results'; import { OutputHashing } from '../application/schema'; -import { findTests, getTestEntrypoints } from './find-tests'; +import { AngularAssetsMiddleware } from './assets-middleware'; +import { createInstrumentationFilter, getInstrumentationExcludedPaths } from './coverage'; +import { getBaseKarmaOptions } from './karma-config'; import { NormalizedKarmaBuilderOptions, normalizeOptions } from './options'; +import { AngularPolyfillsPlugin } from './polyfills-plugin'; +import { injectKarmaReporter } from './progress-reporter'; import { Schema as KarmaBuilderOptions } from './schema'; +import { + collectEntrypoints, + first, + getProjectSourceRoot, + hasChunkOrWorkerFiles, + normalizePolyfills, +} from './utils'; import type { KarmaBuilderTransformsOptions } from './index'; -const localResolve = createRequire(__filename).resolve; -const isWindows = process.platform === 'win32'; - interface BuildOptions extends ApplicationBuilderInternalOptions { // We know that it's always a string since we set it. outputPath: string; @@ -43,239 +47,6 @@ class ApplicationBuildError extends Error { } } -interface ServeFileFunction { - ( - filepath: string, - rangeHeader: string | string[] | undefined, - response: ServerResponse, - transform?: (c: string | Uint8Array) => string | Uint8Array, - content?: string | Uint8Array, - doNotCache?: boolean, - ): void; -} - -interface LatestBuildFiles { - files: Record; -} - -const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles'; - -class AngularAssetsMiddleware { - static readonly $inject = ['serveFile', LATEST_BUILD_FILES_TOKEN]; - - static readonly NAME = 'angular-test-assets'; - - constructor( - private readonly serveFile: ServeFileFunction, - private readonly latestBuildFiles: LatestBuildFiles, - ) {} - - handle(req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => unknown) { - const url = new URL(`http://${req.headers['host']}${req.url}`); - // Remove the leading slash from the URL path and convert to platform specific. - // The latest build files will use the platform path separator. - let pathname = url.pathname.slice(1); - if (isWindows) { - pathname = pathname.replaceAll(path.posix.sep, path.win32.sep); - } - - const file = this.latestBuildFiles.files[pathname]; - if (!file) { - next(); - - return; - } - - // Implementation of serverFile can be found here: - // https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10 - switch (file.origin) { - case 'disk': - this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true); - break; - case 'memory': - // Include pathname to help with Content-Type headers. - this.serveFile( - `/unused/${url.pathname}`, - undefined, - res, - undefined, - file.contents, - /* doNotCache */ false, - ); - break; - } - } - - static createPlugin(initialFiles: LatestBuildFiles): InlinePluginDef { - return { - [LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }], - - [`middleware:${AngularAssetsMiddleware.NAME}`]: [ - 'factory', - Object.assign((...args: ConstructorParameters) => { - const inst = new AngularAssetsMiddleware(...args); - - return inst.handle.bind(inst); - }, AngularAssetsMiddleware), - ], - }; - } -} - -class AngularPolyfillsPlugin { - static readonly $inject = ['config.files']; - - static readonly NAME = 'angular-polyfills'; - - static createPlugin( - polyfillsFile: FilePattern, - jasmineCleanupFiles: FilePattern, - scriptsFiles: FilePattern[], - ): InlinePluginDef { - return { - // This has to be a "reporter" because reporters run _after_ frameworks - // and karma-jasmine-html-reporter injects additional scripts that may - // depend on Jasmine but aren't modules - which means that they would run - // _before_ all module code (including jasmine). - [`reporter:${AngularPolyfillsPlugin.NAME}`]: [ - 'factory', - Object.assign((files: (string | FilePattern)[]) => { - // The correct order is zone.js -> jasmine -> zone.js/testing. - // Jasmine has to see the patched version of the global `setTimeout` - // function so it doesn't cache the unpatched version. And /testing - // needs to see the global `jasmine` object so it can patch it. - const polyfillsIndex = 0; - files.splice(polyfillsIndex, 0, polyfillsFile); - - // Insert just before test_main.js. - const zoneTestingIndex = files.findIndex((f) => { - if (typeof f === 'string') { - return false; - } - - return f.pattern.endsWith('/test_main.js'); - }); - if (zoneTestingIndex === -1) { - throw new Error('Could not find test entrypoint file.'); - } - files.splice(zoneTestingIndex, 0, jasmineCleanupFiles); - - // We need to ensure that all files are served as modules, otherwise - // the order in the files list gets really confusing: Karma doesn't - // set defer on scripts, so all scripts with type=js will run first, - // even if type=module files appeared earlier in `files`. - for (const f of files) { - if (typeof f === 'string') { - throw new Error(`Unexpected string-based file: "${f}"`); - } - if (f.included === false) { - // Don't worry about files that aren't included on the initial - // page load. `type` won't affect them. - continue; - } - if (f.pattern.endsWith('.js') && 'js' === (f.type ?? 'js')) { - f.type = 'module'; - } - } - - // Add "scripts" option files as classic scripts - files.unshift(...scriptsFiles); - - // Add browser sourcemap support as a classic script - files.unshift({ - pattern: localResolve('source-map-support/browser-source-map-support.js'), - included: true, - watched: false, - }); - }, AngularPolyfillsPlugin), - ], - }; - } -} - -function injectKarmaReporter( - buildOptions: BuildOptions, - buildIterator: AsyncIterator, - karmaConfig: Config & ConfigOptions, - controller: ReadableStreamController, -) { - const reporterName = 'angular-progress-notifier'; - - interface RunCompleteInfo { - exitCode: number; - } - - interface KarmaEmitter { - refreshFiles(): void; - } - - class ProgressNotifierReporter { - static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN]; - - constructor( - private readonly emitter: KarmaEmitter, - private readonly latestBuildFiles: LatestBuildFiles, - ) { - this.startWatchingBuild(); - } - - private startWatchingBuild() { - void (async () => { - // This is effectively "for await of but skip what's already consumed". - let isDone = false; // to mark the loop condition as "not constant". - while (!isDone) { - const { done, value: buildOutput } = await buildIterator.next(); - if (done) { - isDone = true; - break; - } - - if (buildOutput.kind === ResultKind.Failure) { - controller.enqueue({ success: false, message: 'Build failed' }); - } else if ( - buildOutput.kind === ResultKind.Incremental || - buildOutput.kind === ResultKind.Full - ) { - if (buildOutput.kind === ResultKind.Full) { - this.latestBuildFiles.files = buildOutput.files; - } else { - this.latestBuildFiles.files = { - ...this.latestBuildFiles.files, - ...buildOutput.files, - }; - } - await writeTestFiles(buildOutput.files, buildOptions.outputPath); - this.emitter.refreshFiles(); - } - } - })(); - } - - onRunComplete = function (_browsers: unknown, results: RunCompleteInfo) { - if (results.exitCode === 0) { - controller.enqueue({ success: true }); - } else { - controller.enqueue({ success: false }); - } - }; - } - - karmaConfig.reporters ??= []; - karmaConfig.reporters.push(reporterName); - - karmaConfig.plugins ??= []; - karmaConfig.plugins.push({ - [`reporter:${reporterName}`]: [ - 'factory', - Object.assign( - (...args: ConstructorParameters) => - new ProgressNotifierReporter(...args), - ProgressNotifierReporter, - ), - ], - }); -} - export function execute( options: KarmaBuilderOptions, context: BuilderContext, @@ -324,54 +95,6 @@ export function execute( }); } -async function getProjectSourceRoot(context: BuilderContext): Promise { - // We have already validated that the project name is set before calling this function. - const projectName = context.target?.project; - if (!projectName) { - return context.workspaceRoot; - } - - const projectMetadata = await context.getProjectMetadata(projectName); - const { projectSourceRoot } = getProjectRootPaths(context.workspaceRoot, projectMetadata); - - return projectSourceRoot; -} - -function normalizePolyfills( - polyfills: string[] = [], -): [polyfills: string[], jasmineCleanup: string[]] { - const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js'); - const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js'); - const sourcemapEntrypoint = localResolve('./polyfills/init_sourcemaps.js'); - - const zoneTestingEntryPoint = 'zone.js/testing'; - const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint); - - return [ - polyfillsExludingZoneTesting.concat([jasmineGlobalEntryPoint, sourcemapEntrypoint]), - polyfillsExludingZoneTesting.length === polyfills.length - ? [jasmineGlobalCleanupEntrypoint] - : [jasmineGlobalCleanupEntrypoint, zoneTestingEntryPoint], - ]; -} - -async function collectEntrypoints( - options: NormalizedKarmaBuilderOptions, - context: BuilderContext, - projectSourceRoot: string, -): Promise> { - // Glob for files to test. - const testFiles = await findTests( - options.include, - options.exclude, - context.workspaceRoot, - projectSourceRoot, - ); - - return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot }); -} - -// eslint-disable-next-line max-lines-per-function async function initializeApplication( options: NormalizedKarmaBuilderOptions, context: BuilderContext, @@ -380,14 +103,48 @@ async function initializeApplication( ): Promise< [typeof import('karma'), Config & ConfigOptions, BuildOptions, AsyncIterator | null] > { - const outputPath = path.join(context.workspaceRoot, 'dist/test-out', randomUUID()); + const karma = await import('karma'); const projectSourceRoot = await getProjectSourceRoot(context); - const [karma, entryPoints] = await Promise.all([ - import('karma'), - collectEntrypoints(options, context, projectSourceRoot), - fs.rm(outputPath, { recursive: true, force: true }), - ]); + // Setup temporary output path and ensure it is empty + const outputPath = path.join(context.workspaceRoot, 'dist/test-out', randomUUID()); + await fs.rm(outputPath, { recursive: true, force: true }); + // Setup exit cleanup for temporary directory + const handleProcessExit = () => rmSync(outputPath, { recursive: true, force: true }); + process.once('exit', handleProcessExit); + process.once('SIGINT', handleProcessExit); + process.once('uncaughtException', handleProcessExit); + + const { buildOptions, mainName } = await setupBuildOptions( + options, + context, + projectSourceRoot, + outputPath, + ); + + const [buildOutput, buildIterator] = await runEsbuild(buildOptions, context, projectSourceRoot); + + const karmaConfig = await configureKarma( + karma, + context, + karmaOptions, + options, + buildOptions, + buildOutput, + mainName, + transforms, + ); + + return [karma, karmaConfig, buildOptions, buildIterator]; +} + +async function setupBuildOptions( + options: NormalizedKarmaBuilderOptions, + context: BuilderContext, + projectSourceRoot: string, + outputPath: string, +): Promise<{ buildOptions: BuildOptions; mainName: string }> { + const entryPoints = await collectEntrypoints(options, context, projectSourceRoot); const mainName = 'test_main'; if (options.main) { @@ -433,17 +190,29 @@ async function initializeApplication( externalDependencies: options.externalDependencies, }; + return { buildOptions, mainName }; +} + +async function runEsbuild( + buildOptions: BuildOptions, + context: BuilderContext, + projectSourceRoot: string, +): Promise<[Result & { kind: ResultKind.Full }, AsyncIterator | null]> { + const usesZoneJS = buildOptions.polyfills?.includes('zone.js'); const virtualTestBedInit = createVirtualModulePlugin({ namespace: 'angular:test-bed-init', loadContent: async () => { const contents: string[] = [ // Initialize the Angular testing environment + `import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';`, `import { getTestBed } from '@angular/core/testing';`, `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`, - `getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`, + `@NgModule({ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}], })`, + `export class TestModule {}`, + `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`, ` errorOnUnknownElements: true,`, ` errorOnUnknownProperties: true,`, - '});', + `});`, ]; return { @@ -470,6 +239,21 @@ async function initializeApplication( // Write test files await writeTestFiles(buildOutput.files, buildOptions.outputPath); + return [buildOutput, buildIterator]; +} + +async function configureKarma( + karma: typeof import('karma'), + context: BuilderContext, + karmaOptions: ConfigOptions, + options: NormalizedKarmaBuilderOptions, + buildOptions: BuildOptions, + buildOutput: Result & { kind: ResultKind.Full }, + mainName: string, + transforms?: KarmaBuilderTransformsOptions, +): Promise { + const outputPath = buildOptions.outputPath; + // We need to add this to the beginning *after* the testing framework has // prepended its files. The output path is required for each since they are // added later in the test process via a plugin. @@ -629,160 +413,5 @@ async function initializeApplication( parsedKarmaConfig.reporters = (parsedKarmaConfig.reporters ?? []).concat(['coverage']); } - return [karma, parsedKarmaConfig, buildOptions, buildIterator]; -} - -function hasChunkOrWorkerFiles(files: Record): boolean { - return Object.keys(files).some((filename) => { - return /(?:^|\/)(?:worker|chunk)[^/]+\.js$/.test(filename); - }); -} - -export async function writeTestFiles(files: Record, testDir: string) { - const directoryExists = new Set(); - // Writes the test related output files to disk and ensures the containing directories are present - await emitFilesToDisk(Object.entries(files), async ([filePath, file]) => { - if (file.type !== BuildOutputFileType.Browser && file.type !== BuildOutputFileType.Media) { - return; - } - - const fullFilePath = path.join(testDir, filePath); - - // Ensure output subdirectories exist - const fileBasePath = path.dirname(fullFilePath); - if (fileBasePath && !directoryExists.has(fileBasePath)) { - await fs.mkdir(fileBasePath, { recursive: true }); - directoryExists.add(fileBasePath); - } - - if (file.origin === 'memory') { - // Write file contents - await fs.writeFile(fullFilePath, file.contents); - } else { - // Copy file contents - await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE); - } - }); -} - -/** Returns the first item yielded by the given generator and cancels the execution. */ -async function first( - generator: AsyncIterable, - { cancel }: { cancel: boolean }, -): Promise<[T, AsyncIterator | null]> { - if (!cancel) { - const iterator: AsyncIterator = generator[Symbol.asyncIterator](); - const firstValue = await iterator.next(); - if (firstValue.done) { - throw new Error('Expected generator to emit at least once.'); - } - - return [firstValue.value, iterator]; - } - - for await (const value of generator) { - return [value, null]; - } - - throw new Error('Expected generator to emit at least once.'); -} - -function createInstrumentationFilter(includedBasePath: string, excludedPaths: Set) { - return (request: string): boolean => { - return ( - !excludedPaths.has(request) && - !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(request) && - request.startsWith(includedBasePath) - ); - }; -} - -function getInstrumentationExcludedPaths(root: string, excludedPaths: string[]): Set { - const excluded = new Set(); - - for (const excludeGlob of excludedPaths) { - const excludePath = excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob; - globSync(excludePath, { cwd: root }).forEach((p) => excluded.add(path.join(root, p))); - } - - return excluded; -} -function getBaseKarmaOptions( - options: NormalizedKarmaBuilderOptions, - context: BuilderContext, -): ConfigOptions { - // Determine project name from builder context target - const projectName = context.target?.project; - if (!projectName) { - throw new Error(`The 'karma' builder requires a target to be specified.`); - } - - const karmaOptions: ConfigOptions = options.karmaConfig - ? {} - : getBuiltInKarmaConfig(context.workspaceRoot, projectName); - - const singleRun = !options.watch; - karmaOptions.singleRun = singleRun; - - // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default - // for single run executions. Not clearing context for multi-run (watched) builds allows the - // Jasmine Spec Runner to be visible in the browser after test execution. - karmaOptions.client ??= {}; - karmaOptions.client.clearContext ??= singleRun; - - // Convert browsers from a string to an array - if (options.browsers) { - karmaOptions.browsers = options.browsers; - } - - if (options.reporters) { - karmaOptions.reporters = options.reporters; - } - - return karmaOptions; -} - -function getBuiltInKarmaConfig( - workspaceRoot: string, - projectName: string, -): ConfigOptions & Record { - let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName; - coverageFolderName = coverageFolderName.toLowerCase(); - - const workspaceRootRequire = createRequire(workspaceRoot + '/'); - - // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template - return { - basePath: '', - frameworks: ['jasmine'], - plugins: [ - 'karma-jasmine', - 'karma-chrome-launcher', - 'karma-jasmine-html-reporter', - 'karma-coverage', - ].map((p) => workspaceRootRequire(p)), - jasmineHtmlReporter: { - suppressAll: true, // removes the duplicated traces - }, - coverageReporter: { - dir: path.join(workspaceRoot, 'coverage', coverageFolderName), - subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }], - }, - reporters: ['progress', 'kjhtml'], - browsers: ['Chrome'], - customLaunchers: { - // Chrome configured to run in a bazel sandbox. - // Disable the use of the gpu and `/dev/shm` because it causes Chrome to - // crash on some environments. - // See: - // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips - // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t - ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], - }, - }, - restartOnFileChange: true, - }; + return parsedKarmaConfig; } diff --git a/packages/angular/build/src/builders/karma/assets-middleware.ts b/packages/angular/build/src/builders/karma/assets-middleware.ts new file mode 100644 index 000000000000..fd6ce489e583 --- /dev/null +++ b/packages/angular/build/src/builders/karma/assets-middleware.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 + */ + +import type { InlinePluginDef } from 'karma'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import path from 'node:path'; +import type { ResultFile } from '../application/results'; + +const isWindows = process.platform === 'win32'; + +interface ServeFileFunction { + ( + filepath: string, + rangeHeader: string | string[] | undefined, + response: ServerResponse, + transform?: (c: string | Uint8Array) => string | Uint8Array, + content?: string | Uint8Array, + doNotCache?: boolean, + ): void; +} + +export interface LatestBuildFiles { + files: Record; +} + +const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles'; + +export class AngularAssetsMiddleware { + static readonly $inject = ['serveFile', LATEST_BUILD_FILES_TOKEN]; + + static readonly NAME = 'angular-test-assets'; + + constructor( + private readonly serveFile: ServeFileFunction, + private readonly latestBuildFiles: LatestBuildFiles, + ) {} + + handle(req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => unknown): void { + const url = new URL(`http://${req.headers['host'] ?? ''}${req.url ?? ''}`); + // Remove the leading slash from the URL path and convert to platform specific. + // The latest build files will use the platform path separator. + let pathname = url.pathname.slice(1); + if (isWindows) { + pathname = pathname.replaceAll(path.posix.sep, path.win32.sep); + } + + const file = this.latestBuildFiles.files[pathname]; + if (!file) { + next(); + + return; + } + + // Implementation of serverFile can be found here: + // https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10 + switch (file.origin) { + case 'disk': + this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true); + break; + case 'memory': + // Include pathname to help with Content-Type headers. + this.serveFile( + `/unused/${url.pathname}`, + undefined, + res, + undefined, + file.contents, + /* doNotCache */ false, + ); + break; + } + } + + static createPlugin(initialFiles: LatestBuildFiles): InlinePluginDef { + return { + [LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }], + + [`middleware:${AngularAssetsMiddleware.NAME}`]: [ + 'factory', + Object.assign((...args: ConstructorParameters) => { + const inst = new AngularAssetsMiddleware(...args); + + return inst.handle.bind(inst); + }, AngularAssetsMiddleware), + ], + }; + } +} diff --git a/packages/angular/build/src/builders/karma/coverage.ts b/packages/angular/build/src/builders/karma/coverage.ts new file mode 100644 index 000000000000..7a19f57fbebf --- /dev/null +++ b/packages/angular/build/src/builders/karma/coverage.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import path from 'node:path'; +import { globSync } from 'tinyglobby'; + +export function createInstrumentationFilter(includedBasePath: string, excludedPaths: Set) { + return (request: string): boolean => { + return ( + !excludedPaths.has(request) && + !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]|[\\/]\.angular[\\/]/.test(request) && + request.startsWith(includedBasePath) + ); + }; +} + +export function getInstrumentationExcludedPaths( + root: string, + excludedPaths: string[], +): Set { + const excluded = new Set(); + + for (const excludeGlob of excludedPaths) { + const excludePath = excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob; + globSync(excludePath, { cwd: root }).forEach((p) => excluded.add(path.join(root, p))); + } + + return excluded; +} diff --git a/packages/angular/build/src/builders/karma/find-tests.ts b/packages/angular/build/src/builders/karma/find-tests.ts index 67ae410c6125..00468146df5f 100644 --- a/packages/angular/build/src/builders/karma/find-tests.ts +++ b/packages/angular/build/src/builders/karma/find-tests.ts @@ -6,150 +6,27 @@ * found in the LICENSE file at https://angular.dev/license */ -import { PathLike, constants, promises as fs } from 'node:fs'; -import { basename, dirname, extname, join, relative } from 'node:path'; -import { glob, isDynamicPattern } from 'tinyglobby'; -import { toPosixPath } from '../../utils/path'; +import { findTests as findTestsBase } from '../unit-test/test-discovery'; -/* Go through all patterns and find unique list of files */ -export async function findTests( - include: string[], - exclude: string[], - workspaceRoot: string, - projectSourceRoot: string, -): Promise { - const matchingTestsPromises = include.map((pattern) => - findMatchingTests(pattern, exclude, workspaceRoot, projectSourceRoot), - ); - const files = await Promise.all(matchingTestsPromises); - - // Unique file names - return [...new Set(files.flat())]; -} - -interface TestEntrypointsOptions { - projectSourceRoot: string; - workspaceRoot: string; -} - -/** Generate unique bundle names for a set of test files. */ -export function getTestEntrypoints( - testFiles: string[], - { projectSourceRoot, workspaceRoot }: TestEntrypointsOptions, -): Map { - const seen = new Set(); - - return new Map( - Array.from(testFiles, (testFile) => { - const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot]) - // Strip leading dots and path separators. - .replace(/^[./\\]+/, '') - // Replace any path separators with dashes. - .replace(/[/\\]/g, '-'); - const baseName = `spec-${basename(relativePath, extname(relativePath))}`; - let uniqueName = baseName; - let suffix = 2; - while (seen.has(uniqueName)) { - uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1'); - ++suffix; - } - seen.add(uniqueName); - - return [uniqueName, testFile]; - }), - ); -} - -const removeLeadingSlash = (pattern: string): string => { - if (pattern.charAt(0) === '/') { - return pattern.substring(1); - } - - return pattern; -}; +// This file is a compatibility layer that re-exports the test discovery logic from its new location. +// This is necessary to avoid breaking the Karma builder, which still depends on this file. +export { getTestEntrypoints } from '../unit-test/test-discovery'; -const removeRelativeRoot = (path: string, root: string): string => { - if (path.startsWith(root)) { - return path.substring(root.length); - } - - return path; +const removeLeadingSlash = (path: string): string => { + return path.startsWith('/') ? path.substring(1) : path; }; -function removeRoots(path: string, roots: string[]): string { - for (const root of roots) { - if (path.startsWith(root)) { - return path.substring(root.length); - } - } - - return basename(path); -} - -async function findMatchingTests( - pattern: string, - ignore: string[], +export async function findTests( + include: string[], + exclude: string[], workspaceRoot: string, projectSourceRoot: string, ): Promise { - // normalize pattern, glob lib only accepts forward slashes - let normalizedPattern = toPosixPath(pattern); - normalizedPattern = removeLeadingSlash(normalizedPattern); - - const relativeProjectRoot = toPosixPath(relative(workspaceRoot, projectSourceRoot) + '/'); - - // remove relativeProjectRoot to support relative paths from root - // such paths are easy to get when running scripts via IDEs - normalizedPattern = removeRelativeRoot(normalizedPattern, relativeProjectRoot); - - // special logic when pattern does not look like a glob - if (!isDynamicPattern(normalizedPattern)) { - if (await isDirectory(join(projectSourceRoot, normalizedPattern))) { - normalizedPattern = `${normalizedPattern}/**/*.spec.@(ts|tsx)`; - } else { - // see if matching spec file exists - const fileExt = extname(normalizedPattern); - // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts` - const potentialSpec = join( - projectSourceRoot, - dirname(normalizedPattern), - `${basename(normalizedPattern, fileExt)}.spec${fileExt}`, - ); - - if (await exists(potentialSpec)) { - return [potentialSpec]; - } - } - } - - // normalize the patterns in the ignore list - const normalizedIgnorePatternList = ignore.map((pattern: string) => - removeRelativeRoot(removeLeadingSlash(toPosixPath(pattern)), relativeProjectRoot), + // Karma has legacy support for workspace "root-relative" file paths + return findTestsBase( + include.map(removeLeadingSlash), + exclude.map(removeLeadingSlash), + workspaceRoot, + projectSourceRoot, ); - - return glob(normalizedPattern, { - cwd: projectSourceRoot, - absolute: true, - ignore: ['**/node_modules/**', ...normalizedIgnorePatternList], - }); -} - -async function isDirectory(path: PathLike): Promise { - try { - const stats = await fs.stat(path); - - return stats.isDirectory(); - } catch { - return false; - } -} - -async function exists(path: PathLike): Promise { - try { - await fs.access(path, constants.F_OK); - - return true; - } catch { - return false; - } } diff --git a/packages/angular/build/src/builders/karma/find-tests_spec.ts b/packages/angular/build/src/builders/karma/find-tests_spec.ts index 8264539ae9dd..88c97c8575fe 100644 --- a/packages/angular/build/src/builders/karma/find-tests_spec.ts +++ b/packages/angular/build/src/builders/karma/find-tests_spec.ts @@ -62,6 +62,36 @@ describe('getTestEntrypoints', () => { ]), ); }); + + describe('with removeTestExtension enabled', () => { + function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) { + return getTestEntrypoints( + [ + ...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)), + ...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)), + ], + { ...options, removeTestExtension: true }, + ); + } + + it('removes .spec extension', () => { + expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual( + new Map([ + ['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')], + ['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')], + ]), + ); + }); + + it('removes .test extension', () => { + expect(getEntrypoints(['a/b.test.js'], ['c/d.test.js'])).toEqual( + new Map([ + ['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.test.js')], + ['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.test.js')], + ]), + ); + }); + }); }); } }); diff --git a/packages/angular/build/src/builders/karma/karma-config.ts b/packages/angular/build/src/builders/karma/karma-config.ts new file mode 100644 index 000000000000..08e39887bcb1 --- /dev/null +++ b/packages/angular/build/src/builders/karma/karma-config.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 + */ + +import type { BuilderContext } from '@angular-devkit/architect'; +import type { ConfigOptions } from 'karma'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import type { NormalizedKarmaBuilderOptions } from './options'; + +export function getBaseKarmaOptions( + options: NormalizedKarmaBuilderOptions, + context: BuilderContext, +): ConfigOptions { + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + throw new Error(`The 'karma' builder requires a target to be specified.`); + } + + const karmaOptions: ConfigOptions = options.karmaConfig + ? {} + : getBuiltInKarmaConfig(context.workspaceRoot, projectName); + + const singleRun = !options.watch; + karmaOptions.singleRun = singleRun; + + // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default + // for single run executions. Not clearing context for multi-run (watched) builds allows the + // Jasmine Spec Runner to be visible in the browser after test execution. + karmaOptions.client ??= {}; + karmaOptions.client.clearContext ??= singleRun; + + // Convert browsers from a string to an array + if (options.browsers) { + karmaOptions.browsers = options.browsers; + } + + if (options.reporters) { + karmaOptions.reporters = options.reporters; + } + + return karmaOptions; +} + +function getBuiltInKarmaConfig( + workspaceRoot: string, + projectName: string, +): ConfigOptions & Record { + let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName; + coverageFolderName = coverageFolderName.toLowerCase(); + + const workspaceRootRequire = createRequire(workspaceRoot + '/'); + + // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template + return { + basePath: '', + frameworks: ['jasmine'], + plugins: [ + 'karma-jasmine', + 'karma-chrome-launcher', + 'karma-jasmine-html-reporter', + 'karma-coverage', + ].map((p) => workspaceRootRequire(p)), + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: path.join(workspaceRoot, 'coverage', coverageFolderName), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + customLaunchers: { + // Chrome configured to run in a bazel sandbox. + // Disable the use of the gpu and `/dev/shm` because it causes Chrome to + // crash on some environments. + // See: + // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips + // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], + }, + }, + restartOnFileChange: true, + }; +} diff --git a/packages/angular/build/src/builders/karma/polyfills-plugin.ts b/packages/angular/build/src/builders/karma/polyfills-plugin.ts new file mode 100644 index 000000000000..5cf1022766ec --- /dev/null +++ b/packages/angular/build/src/builders/karma/polyfills-plugin.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { FilePattern, InlinePluginDef } from 'karma'; +import { createRequire } from 'node:module'; + +const localResolve = createRequire(__filename).resolve; + +export class AngularPolyfillsPlugin { + static readonly $inject = ['config.files']; + + static readonly NAME = 'angular-polyfills'; + + static createPlugin( + polyfillsFile: FilePattern, + jasmineCleanupFiles: FilePattern, + scriptsFiles: FilePattern[], + ): InlinePluginDef { + return { + // This has to be a "reporter" because reporters run _after_ frameworks + // and karma-jasmine-html-reporter injects additional scripts that may + // depend on Jasmine but aren't modules - which means that they would run + // _before_ all module code (including jasmine). + [`reporter:${AngularPolyfillsPlugin.NAME}`]: [ + 'factory', + Object.assign((files: (string | FilePattern)[]) => { + // The correct order is zone.js -> jasmine -> zone.js/testing. + // Jasmine has to see the patched version of the global `setTimeout` + // function so it doesn't cache the unpatched version. And /testing + // needs to see the global `jasmine` object so it can patch it. + const polyfillsIndex = 0; + files.splice(polyfillsIndex, 0, polyfillsFile); + + // Insert just before test_main.js. + const zoneTestingIndex = files.findIndex((f) => { + if (typeof f === 'string') { + return false; + } + + return f.pattern.endsWith('/test_main.js'); + }); + if (zoneTestingIndex === -1) { + throw new Error('Could not find test entrypoint file.'); + } + files.splice(zoneTestingIndex, 0, jasmineCleanupFiles); + + // We need to ensure that all files are served as modules, otherwise + // the order in the files list gets really confusing: Karma doesn't + // set defer on scripts, so all scripts with type=js will run first, + // even if type=module files appeared earlier in `files`. + for (const f of files) { + if (typeof f === 'string') { + throw new Error(`Unexpected string-based file: "${f}"`); + } + if (f.included === false) { + // Don't worry about files that aren't included on the initial + // page load. `type` won't affect them. + continue; + } + if (f.pattern.endsWith('.js') && 'js' === (f.type ?? 'js')) { + f.type = 'module'; + } + } + + // Add "scripts" option files as classic scripts + files.unshift(...scriptsFiles); + + // Add browser sourcemap support as a classic script + files.unshift({ + pattern: localResolve('source-map-support/browser-source-map-support.js'), + included: true, + watched: false, + }); + + // Karma needs a return value for a factory and Karma's multi-reporter expects an `adapters` array + return { adapters: [] }; + }, AngularPolyfillsPlugin), + ], + }; + } +} diff --git a/packages/angular/build/src/builders/karma/progress-reporter.ts b/packages/angular/build/src/builders/karma/progress-reporter.ts new file mode 100644 index 000000000000..908f1c856e6d --- /dev/null +++ b/packages/angular/build/src/builders/karma/progress-reporter.ts @@ -0,0 +1,108 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { BuilderOutput } from '@angular-devkit/architect'; +import type { Config, ConfigOptions } from 'karma'; +import type { ReadableStreamController } from 'node:stream/web'; +import { writeTestFiles } from '../../utils/test-files'; +import type { ApplicationBuilderInternalOptions } from '../application/options'; +import type { Result } from '../application/results'; +import { ResultKind } from '../application/results'; +import type { LatestBuildFiles } from './assets-middleware'; + +const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles'; + +interface BuildOptions extends ApplicationBuilderInternalOptions { + // We know that it's always a string since we set it. + outputPath: string; +} + +export function injectKarmaReporter( + buildOptions: BuildOptions, + buildIterator: AsyncIterator, + karmaConfig: Config & ConfigOptions, + controller: ReadableStreamController, +): void { + const reporterName = 'angular-progress-notifier'; + + interface RunCompleteInfo { + exitCode: number; + } + + interface KarmaEmitter { + refreshFiles(): void; + } + + class ProgressNotifierReporter { + static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN]; + // Needed for the karma reporter interface, see https://github.com/angular/angular-cli/issues/31629 + adapters = []; + + constructor( + private readonly emitter: KarmaEmitter, + private readonly latestBuildFiles: LatestBuildFiles, + ) { + this.startWatchingBuild(); + } + + private startWatchingBuild() { + void (async () => { + // This is effectively "for await of but skip what's already consumed". + let isDone = false; // to mark the loop condition as "not constant". + while (!isDone) { + const { done, value: buildOutput } = await buildIterator.next(); + if (done) { + isDone = true; + break; + } + + if (buildOutput.kind === ResultKind.Failure) { + controller.enqueue({ success: false, message: 'Build failed' }); + } else if ( + buildOutput.kind === ResultKind.Incremental || + buildOutput.kind === ResultKind.Full + ) { + if (buildOutput.kind === ResultKind.Full) { + this.latestBuildFiles.files = buildOutput.files; + } else { + this.latestBuildFiles.files = { + ...this.latestBuildFiles.files, + ...buildOutput.files, + }; + } + await writeTestFiles(buildOutput.files, buildOptions.outputPath); + this.emitter.refreshFiles(); + } + } + })(); + } + + onRunComplete = function (_browsers: unknown, results: RunCompleteInfo): void { + if (results.exitCode === 0) { + controller.enqueue({ success: true }); + } else { + controller.enqueue({ success: false }); + } + }; + } + + karmaConfig.reporters ??= []; + karmaConfig.reporters.push(reporterName); + + karmaConfig.plugins ??= []; + karmaConfig.plugins.push({ + [`reporter:${reporterName}`]: [ + 'factory', + Object.assign( + (...args: ConstructorParameters) => + new ProgressNotifierReporter(...args), + ProgressNotifierReporter, + ), + ], + }); +} diff --git a/packages/angular/build/src/builders/karma/tests/behavior/code-coverage_spec.ts b/packages/angular/build/src/builders/karma/tests/behavior/code-coverage_spec.ts index 835f48724dbe..9882af429b76 100644 --- a/packages/angular/build/src/builders/karma/tests/behavior/code-coverage_spec.ts +++ b/packages/angular/build/src/builders/karma/tests/behavior/code-coverage_spec.ts @@ -7,7 +7,6 @@ */ import { setTimeout } from 'node:timers/promises'; -import { tags } from '@angular-devkit/core'; import { last, tap } from 'rxjs'; import { execute } from '../../index'; import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup'; @@ -96,12 +95,12 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { await harness.modifyFile('src/app/app.component.ts', (content) => { return content.replace( `title = 'app'`, - tags.stripIndents` - title = 'app'; + ` +title = 'app'; - async foo() { - return 'foo'; - } +async foo() { + return 'foo'; +} `, ); }); diff --git a/packages/angular/build/src/builders/karma/tests/behavior/fake-async_spec.ts b/packages/angular/build/src/builders/karma/tests/behavior/fake-async_spec.ts index 355ddda8ed43..16da8daf2f55 100644 --- a/packages/angular/build/src/builders/karma/tests/behavior/fake-async_spec.ts +++ b/packages/angular/build/src/builders/karma/tests/behavior/fake-async_spec.ts @@ -35,12 +35,14 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { } }`, './src/app/app.component.spec.ts': ` + import { provideZoneChangeDetection } from '@angular/core'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(() => TestBed.configureTestingModule({ + providers: [provideZoneChangeDetection()], declarations: [AppComponent] })); diff --git a/packages/angular/build/src/builders/karma/tests/behavior/module-cjs_spec.ts b/packages/angular/build/src/builders/karma/tests/behavior/module-cjs_spec.ts index d5c1c7b3d134..646ee6b605ee 100644 --- a/packages/angular/build/src/builders/karma/tests/behavior/module-cjs_spec.ts +++ b/packages/angular/build/src/builders/karma/tests/behavior/module-cjs_spec.ts @@ -22,6 +22,7 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { await harness.modifyFile('src/tsconfig.spec.json', (content) => { const tsConfig = JSON.parse(content); + tsConfig.compilerOptions.moduleResolution = 'bundler'; tsConfig.compilerOptions.module = 'commonjs'; return JSON.stringify(tsConfig); diff --git a/packages/angular/build/src/builders/karma/utils.ts b/packages/angular/build/src/builders/karma/utils.ts new file mode 100644 index 000000000000..6ce8e33ed9aa --- /dev/null +++ b/packages/angular/build/src/builders/karma/utils.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { BuilderContext } from '@angular-devkit/architect'; +import { createRequire } from 'node:module'; +import { BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { getProjectRootPaths } from '../../utils/project-metadata'; +import { findTests, getTestEntrypoints } from './find-tests'; +import type { NormalizedKarmaBuilderOptions } from './options'; + +const localResolve = createRequire(__filename).resolve; + +export async function getProjectSourceRoot(context: BuilderContext): Promise { + // We have already validated that the project name is set before calling this function. + const projectName = context.target?.project; + if (!projectName) { + return context.workspaceRoot; + } + + const projectMetadata = await context.getProjectMetadata(projectName); + const { projectSourceRoot } = getProjectRootPaths(context.workspaceRoot, projectMetadata); + + return projectSourceRoot; +} + +export function normalizePolyfills( + polyfills: string[] = [], +): [polyfills: string[], jasmineCleanup: string[]] { + const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js'); + const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js'); + const sourcemapEntrypoint = localResolve('./polyfills/init_sourcemaps.js'); + + const zoneTestingEntryPoint = 'zone.js/testing'; + const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint); + + return [ + polyfillsExludingZoneTesting.concat([jasmineGlobalEntryPoint, sourcemapEntrypoint]), + polyfillsExludingZoneTesting.length === polyfills.length + ? [jasmineGlobalCleanupEntrypoint] + : [jasmineGlobalCleanupEntrypoint, zoneTestingEntryPoint], + ]; +} + +export async function collectEntrypoints( + options: NormalizedKarmaBuilderOptions, + context: BuilderContext, + projectSourceRoot: string, +): Promise> { + // Glob for files to test. + const testFiles = await findTests( + options.include, + options.exclude, + context.workspaceRoot, + projectSourceRoot, + ); + + return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot }); +} + +export function hasChunkOrWorkerFiles(files: Record): boolean { + return Object.keys(files).some((filename) => { + return /(?:^|\/)(?:worker|chunk)[^/]+\.js$/.test(filename); + }); +} + +/** Returns the first item yielded by the given generator and cancels the execution. */ +export async function first( + generator: AsyncIterable, + { cancel }: { cancel: boolean }, +): Promise<[T, AsyncIterator | null]> { + if (!cancel) { + const iterator: AsyncIterator = generator[Symbol.asyncIterator](); + const firstValue = await iterator.next(); + if (firstValue.done) { + throw new Error('Expected generator to emit at least once.'); + } + + return [firstValue.value, iterator]; + } + + for await (const value of generator) { + return [value, null]; + } + + throw new Error('Expected generator to emit at least once.'); +} diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index dde40f449742..122039595bd9 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -6,423 +6,332 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { + type BuilderContext, + type BuilderOutput, + targetStringFromTarget, +} from '@angular-devkit/architect'; import assert from 'node:assert'; -import { randomUUID } from 'node:crypto'; -import { createRequire } from 'node:module'; +import { rm } from 'node:fs/promises'; import path from 'node:path'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; import { assertIsError } from '../../utils/error'; -import { loadEsmModule } from '../../utils/load-esm'; -import { toPosixPath } from '../../utils/path'; +import { writeTestFiles } from '../../utils/test-files'; import { buildApplicationInternal } from '../application'; import type { ApplicationBuilderExtensions, ApplicationBuilderInternalOptions, } from '../application/options'; import { ResultKind } from '../application/results'; -import { OutputHashing } from '../application/schema'; -import { writeTestFiles } from '../karma/application_builder'; -import { findTests, getTestEntrypoints } from '../karma/find-tests'; -import { useKarmaBuilder } from './karma-bridge'; -import { - NormalizedUnitTestBuilderOptions, - injectTestingPolyfills, - normalizeOptions, -} from './options'; +import { normalizeOptions } from './options'; +import type { TestRunner } from './runners/api'; +import { MissingDependenciesError } from './runners/dependency-checker'; import type { Schema as UnitTestBuilderOptions } from './schema'; +import { findTests } from './test-discovery'; export type { UnitTestBuilderOptions }; -type VitestCoverageOption = Exclude; +async function loadTestRunner(runnerName: string): Promise { + // Harden against directory traversal + if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) { + throw new Error( + `Invalid runner name "${runnerName}". Runner names can only contain alphanumeric characters and hyphens.`, + ); + } + + let runnerModule; + try { + runnerModule = await import(`./runners/${runnerName}/index`); + } catch (e) { + assertIsError(e); + if (e.code === 'ERR_MODULE_NOT_FOUND') { + throw new Error(`Unknown test runner "${runnerName}".`); + } + throw new Error( + `Failed to load the '${runnerName}' test runner. The package may be corrupted or improperly installed.\n` + + `Error: ${e.message}`, + ); + } + + const runner = runnerModule.default; + if ( + !runner || + typeof runner.getBuildOptions !== 'function' || + typeof runner.createExecutor !== 'function' + ) { + throw new Error( + `The loaded test runner '${runnerName}' does not appear to be a valid TestRunner implementation.`, + ); + } + + return runner; +} + +function prepareBuildExtensions( + virtualFiles: Record | undefined, + projectSourceRoot: string, + extensions?: ApplicationBuilderExtensions, +): ApplicationBuilderExtensions | undefined { + if (!virtualFiles) { + return extensions; + } + + extensions ??= {}; + extensions.codePlugins ??= []; + for (const [namespace, contents] of Object.entries(virtualFiles)) { + extensions.codePlugins.push( + createVirtualModulePlugin({ + namespace, + loadContent: () => { + return { + contents, + loader: 'js', + resolveDir: projectSourceRoot, + }; + }, + }), + ); + } + + return extensions; +} + +async function* runBuildAndTest( + executor: import('./runners/api').TestExecutor, + applicationBuildOptions: ApplicationBuilderInternalOptions, + context: BuilderContext, + dumpDirectory: string | undefined, + extensions: ApplicationBuilderExtensions | undefined, +): AsyncIterable { + let consecutiveErrorCount = 0; + for await (const buildResult of buildApplicationInternal( + applicationBuildOptions, + context, + extensions, + )) { + if (buildResult.kind === ResultKind.Failure) { + yield { success: false }; + continue; + } else if ( + buildResult.kind !== ResultKind.Full && + buildResult.kind !== ResultKind.Incremental + ) { + assert.fail( + 'A full and/or incremental build result is required from the application builder.', + ); + } + + assert(buildResult.files, 'Builder did not provide result files.'); + + if (dumpDirectory) { + if (buildResult.kind === ResultKind.Full) { + // Full build, so clean the directory + await rm(dumpDirectory, { recursive: true, force: true }); + } else { + // Incremental build, so delete removed files + for (const file of buildResult.removed) { + await rm(path.join(dumpDirectory, file.path), { force: true }); + } + } + await writeTestFiles(buildResult.files, dumpDirectory); + context.logger.info(`Build output files successfully dumped to '${dumpDirectory}'.`); + } + + // Pass the build artifacts to the executor + try { + yield* executor.execute(buildResult); + + // Successful execution resets the failure counter + consecutiveErrorCount = 0; + } catch (e) { + assertIsError(e); + context.logger.error(`An exception occurred during test execution:\n${e.stack ?? e.message}`); + if (e instanceof AggregateError) { + e.errors.forEach((inner) => { + assertIsError(inner); + context.logger.error(inner.stack ?? inner.message); + }); + } + + yield { success: false }; + consecutiveErrorCount++; + } + + if (consecutiveErrorCount >= 3) { + context.logger.error( + 'Test runner process has failed multiple times in a row. Please fix the configuration and restart the process.', + ); + + return; + } + } +} /** * @experimental Direct usage of this function is considered experimental. */ -// eslint-disable-next-line max-lines-per-function export async function* execute( options: UnitTestBuilderOptions, context: BuilderContext, - extensions: ApplicationBuilderExtensions = {}, + extensions?: ApplicationBuilderExtensions, ): AsyncIterable { // Determine project name from builder context target const projectName = context.target?.project; if (!projectName) { - context.logger.error( - `The "${context.builder.builderName}" builder requires a target to be specified.`, - ); + context.logger.error(`The builder requires a target to be specified.`); return; } - context.logger.warn( - `NOTE: The "${context.builder.builderName}" builder is currently EXPERIMENTAL and not ready for production use.`, - ); - - const normalizedOptions = await normalizeOptions(context, projectName, options); - const { projectSourceRoot, workspaceRoot, runnerName } = normalizedOptions; - - // Translate options and use karma builder directly if specified - if (runnerName === 'karma') { - const karmaBridge = await useKarmaBuilder(context, normalizedOptions); - yield* karmaBridge; + // Initialize the test runner and normalize options + let runner; + let normalizedOptions; + try { + normalizedOptions = await normalizeOptions(context, projectName, options); + runner = await loadTestRunner(normalizedOptions.runnerName); + await runner.validateDependencies?.(normalizedOptions); + } catch (e) { + assertIsError(e); + if (e instanceof MissingDependenciesError) { + context.logger.error(e.message); + } else { + context.logger.error( + `An exception occurred during initialization of the test runner:\n${e.stack ?? e.message}`, + ); + } + yield { success: false }; return; } - if (runnerName !== 'vitest') { - context.logger.error('Unknown test runner: ' + runnerName); - - return; - } + if (normalizedOptions.listTests) { + const testFiles = await findTests( + normalizedOptions.include, + normalizedOptions.exclude ?? [], + normalizedOptions.workspaceRoot, + normalizedOptions.projectSourceRoot, + ); - // Find test files - const testFiles = await findTests( - normalizedOptions.include, - normalizedOptions.exclude, - workspaceRoot, - projectSourceRoot, - ); + context.logger.info('Discovered test files:'); + for (const file of testFiles) { + context.logger.info(` ${path.relative(normalizedOptions.workspaceRoot, file)}`); + } - if (testFiles.length === 0) { - context.logger.error('No tests found.'); + yield { success: true }; - return { success: false }; + return; } - const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot }); - entryPoints.set('init-testbed', 'angular:test-bed-init'); - - let vitestNodeModule; - try { - vitestNodeModule = await loadEsmModule('vitest/node'); - } catch (error: unknown) { - assertIsError(error); - if (error.code !== 'ERR_MODULE_NOT_FOUND') { - throw error; + if (runner.isStandalone) { + try { + await using executor = await runner.createExecutor(context, normalizedOptions, undefined); + yield* executor.execute({ + kind: ResultKind.Full, + files: {}, + }); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred during standalone test execution:\n${e.stack ?? e.message}`, + ); + yield { success: false }; } - context.logger.error( - 'The `vitest` package was not found. Please install the package and rerun the test command.', - ); - return; } - const { startVitest } = vitestNodeModule; - - // Setup test file build options based on application build target options - const buildTargetOptions = (await context.validateOptions( - await context.getTargetOptions(normalizedOptions.buildTarget), - await context.getBuilderNameForTarget(normalizedOptions.buildTarget), - )) as unknown as ApplicationBuilderInternalOptions; - - buildTargetOptions.polyfills = injectTestingPolyfills(buildTargetOptions.polyfills); - - const outputPath = toPosixPath(path.join(context.workspaceRoot, generateOutputPath())); - const buildOptions: ApplicationBuilderInternalOptions = { - ...buildTargetOptions, - watch: normalizedOptions.watch, - incrementalResults: normalizedOptions.watch, - outputPath, - index: false, - browser: undefined, - server: undefined, - outputMode: undefined, - localize: false, - budgets: [], - serviceWorker: false, - appShell: false, - ssr: false, - prerender: false, - sourceMap: { scripts: true, vendor: false, styles: false }, - outputHashing: OutputHashing.None, - optimization: false, - tsConfig: normalizedOptions.tsConfig, - entryPoints, - externalDependencies: [ - 'vitest', - '@vitest/browser/context', - ...(buildTargetOptions.externalDependencies ?? []), - ], - }; - extensions ??= {}; - extensions.codePlugins ??= []; - const virtualTestBedInit = createVirtualModulePlugin({ - namespace: 'angular:test-bed-init', - loadContent: async () => { - const contents: string[] = [ - // Initialize the Angular testing environment - `import { NgModule } from '@angular/core';`, - `import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';`, - `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`, - '', - normalizedOptions.providersFile - ? `import providers from './${toPosixPath( - path - .relative(projectSourceRoot, normalizedOptions.providersFile) - .replace(/.[mc]?ts$/, ''), - )}'` - : 'const providers = [];', - '', - // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/src/test_hooks.ts#L21-L29 - `beforeEach(getCleanupHook(false));`, - `afterEach(getCleanupHook(true));`, - '', - `@NgModule({`, - ` providers,`, - `})`, - `export class TestModule {}`, - '', - `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`, - ` errorOnUnknownElements: true,`, - ` errorOnUnknownProperties: true,`, - '});', - ]; - - return { - contents: contents.join('\n'), - loader: 'js', - resolveDir: projectSourceRoot, - }; - }, - }); - extensions.codePlugins.unshift(virtualTestBedInit); - - let instance: import('vitest/node').Vitest | undefined; - - // Setup vitest browser options if configured - const { browser, errors } = setupBrowserConfiguration( - normalizedOptions.browsers, - normalizedOptions.debug, - projectSourceRoot, - ); - if (errors?.length) { - errors.forEach((error) => context.logger.error(error)); - - return { success: false }; - } - - // Add setup file entries for TestBed initialization and project polyfills - const setupFiles = ['init-testbed.js', ...normalizedOptions.setupFiles]; - if (buildTargetOptions?.polyfills?.length) { - // Placed first as polyfills may be required by the Testbed initialization - // or other project provided setup files (e.g., zone.js, ECMAScript polyfills). - setupFiles.unshift('polyfills.js'); - } - const debugOptions = normalizedOptions.debug - ? { - inspectBrk: true, - isolate: false, - fileParallelism: false, - } - : {}; + // Get base build options from the buildTarget + let buildTargetOptions: ApplicationBuilderInternalOptions; try { - for await (const result of buildApplicationInternal(buildOptions, context, extensions)) { - if (result.kind === ResultKind.Failure) { - continue; - } else if (result.kind !== ResultKind.Full && result.kind !== ResultKind.Incremental) { - assert.fail( - 'A full and/or incremental build result is required from the application builder.', - ); - } - assert(result.files, 'Builder did not provide result files.'); - - await writeTestFiles(result.files, outputPath); - - instance ??= await startVitest( - 'test', - undefined /* cliFilters */, - { - // Disable configuration file resolution/loading - config: false, - root: workspaceRoot, - project: ['base', projectName], - name: 'base', - include: [], - reporters: normalizedOptions.reporters ?? ['default'], - watch: normalizedOptions.watch, - coverage: generateCoverageOption( - normalizedOptions.codeCoverage, - workspaceRoot, - outputPath, - ), - ...debugOptions, - }, - { - plugins: [ - { - name: 'angular:project-init', - async configureVitest(context) { - // Create a subproject that can be configured with plugins for browser mode. - // Plugins defined directly in the vite overrides will not be present in the - // browser specific Vite instance. - const [project] = await context.injectTestProjects({ - test: { - name: projectName, - root: outputPath, - globals: true, - setupFiles, - // Use `jsdom` if no browsers are explicitly configured. - // `node` is effectively no "environment" and the default. - environment: browser ? 'node' : 'jsdom', - browser, - }, - plugins: [ - { - name: 'angular:html-index', - transformIndexHtml() { - // Add all global stylesheets - return ( - Object.entries(result.files) - // TODO: Expand this to all configured global stylesheets - .filter(([file]) => file === 'styles.css') - .map(([styleUrl]) => ({ - tag: 'link', - attrs: { - 'href': styleUrl, - 'rel': 'stylesheet', - }, - injectTo: 'head', - })) - ); - }, - }, - ], - }); - - // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests. - // Vite does this as a convenience but is problematic for the bundling strategy employed by the - // builder's test setup. To workaround this, the excludes are adjusted here to only automaticallyAdd commentMore actions - // exclude the TypeScript source test files. - project.config.coverage.exclude = [ - ...(normalizedOptions.codeCoverage?.exclude ?? []), - '**/*.{test,spec}.?(c|m)ts', - ]; - }, - }, - ], - }, + const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); + if ( + builderName !== '@angular/build:application' && + // TODO: Add comprehensive support for ng-packagr. + builderName !== '@angular/build:ng-packagr' + ) { + context.logger.warn( + `The 'buildTarget' is configured to use '${builderName}', which is not supported. ` + + `The 'unit-test' builder is designed to work with '@angular/build:application'. ` + + 'Unexpected behavior or build failures may occur.', ); - - // Check if all the tests pass to calculate the result - const testModules = instance.state.getTestModules(); - - yield { success: testModules.every((testModule) => testModule.ok()) }; } - } finally { - if (normalizedOptions.watch) { - // Vitest will automatically close if not using watch mode - await instance?.close(); - } - } -} -function findBrowserProvider( - projectResolver: NodeJS.RequireResolve, -): import('vitest/node').BrowserBuiltinProvider | undefined { - // One of these must be installed in the project to use browser testing - const vitestBuiltinProviders = ['playwright', 'webdriverio'] as const; - - for (const providerName of vitestBuiltinProviders) { - try { - projectResolver(providerName); + buildTargetOptions = (await context.validateOptions( + await context.getTargetOptions(normalizedOptions.buildTarget), + builderName, + )) as unknown as ApplicationBuilderInternalOptions; + } catch (e) { + assertIsError(e); + context.logger.error( + `Could not load build target options for "${targetStringFromTarget( + normalizedOptions.buildTarget, + )}".\n` + + `Please check your 'angular.json' configuration.\n` + + `Error: ${e.message}`, + ); + yield { success: false }; - return providerName; - } catch {} + return; } -} - -function normalizeBrowserName(browserName: string): string { - // Normalize browser names to match Vitest's expectations for headless but also supports karma's names - // e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' - // and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'. - const normalized = browserName.toLowerCase(); - return normalized.replace(/headless$/, ''); -} + // Get runner-specific build options + let runnerBuildOptions; + let virtualFiles; + let testEntryPointMappings; + try { + ({ + buildOptions: runnerBuildOptions, + virtualFiles, + testEntryPointMappings, + } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions)); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred while getting runner-specific build options:\n${e.stack ?? e.message}`, + ); + yield { success: false }; -function setupBrowserConfiguration( - browsers: string[] | undefined, - debug: boolean, - projectSourceRoot: string, -): { browser?: import('vitest/node').BrowserConfigOptions; errors?: string[] } { - if (browsers === undefined) { - return {}; + return; } - const projectResolver = createRequire(projectSourceRoot + '/').resolve; - let errors: string[] | undefined; - try { - projectResolver('@vitest/browser'); - } catch { - errors ??= []; - errors.push( - 'The "browsers" option requires the "@vitest/browser" package to be installed within the project.' + - ' Please install this package and rerun the test command.', + await using executor = await runner.createExecutor( + context, + normalizedOptions, + testEntryPointMappings, ); - } - const provider = findBrowserProvider(projectResolver); - if (!provider) { - errors ??= []; - errors.push( - 'The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' + - ' Please install one of these packages and rerun the test command.', + const finalExtensions = prepareBuildExtensions( + virtualFiles, + normalizedOptions.projectSourceRoot, + extensions, ); - } - // Vitest current requires the playwright browser provider to use the inspect-brk option used by "debug" - if (debug && provider !== 'playwright') { - errors ??= []; - errors.push( - 'Debugging browser mode tests currently requires the use of "playwright".' + - ' Please install this package and rerun the test command.', + // Prepare and run the application build + const applicationBuildOptions = { + ...buildTargetOptions, + ...runnerBuildOptions, + watch: normalizedOptions.watch, + progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress, + ...(normalizedOptions.tsConfig ? { tsConfig: normalizedOptions.tsConfig } : {}), + } satisfies ApplicationBuilderInternalOptions; + + const dumpDirectory = normalizedOptions.dumpVirtualFiles + ? path.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files') + : undefined; + + yield* runBuildAndTest( + executor, + applicationBuildOptions, + context, + dumpDirectory, + finalExtensions, ); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred while creating the test executor:\n${e.stack ?? e.message}`, + ); + yield { success: false }; } - - if (errors) { - return { errors }; - } - - const browser = { - enabled: true, - provider, - headless: browsers.some((name) => name.toLowerCase().includes('headless')), - - instances: browsers.map((browserName) => ({ - browser: normalizeBrowserName(browserName), - })), - }; - - return { browser }; -} - -function generateOutputPath(): string { - const datePrefix = new Date().toISOString().replaceAll(/[-:.]/g, ''); - const uuidSuffix = randomUUID().slice(0, 8); - - return path.join('dist', 'test-out', `${datePrefix}-${uuidSuffix}`); -} -function generateCoverageOption( - codeCoverage: NormalizedUnitTestBuilderOptions['codeCoverage'], - workspaceRoot: string, - outputPath: string, -): VitestCoverageOption { - if (!codeCoverage) { - return { - enabled: false, - }; - } - - return { - enabled: true, - excludeAfterRemap: true, - include: [`${toPosixPath(path.relative(workspaceRoot, outputPath))}/**`], - // Special handling for `reporter` due to an undefined value causing upstream failures - ...(codeCoverage.reporters - ? ({ reporter: codeCoverage.reporters } satisfies VitestCoverageOption) - : {}), - }; } diff --git a/packages/angular/build/src/builders/unit-test/karma-bridge.ts b/packages/angular/build/src/builders/unit-test/karma-bridge.ts deleted file mode 100644 index 7bfe9a1ebe94..000000000000 --- a/packages/angular/build/src/builders/unit-test/karma-bridge.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import type { ApplicationBuilderInternalOptions } from '../application/options'; -import type { KarmaBuilderOptions } from '../karma'; -import { type NormalizedUnitTestBuilderOptions, injectTestingPolyfills } from './options'; - -export async function useKarmaBuilder( - context: BuilderContext, - unitTestOptions: NormalizedUnitTestBuilderOptions, -): Promise> { - if (unitTestOptions.debug) { - context.logger.warn( - 'The "karma" test runner does not support the "debug" option. The option will be ignored.', - ); - } - - if (unitTestOptions.setupFiles.length) { - context.logger.warn( - 'The "karma" test runner does not support the "setupFiles" option. The option will be ignored.', - ); - } - - const buildTargetOptions = (await context.validateOptions( - await context.getTargetOptions(unitTestOptions.buildTarget), - await context.getBuilderNameForTarget(unitTestOptions.buildTarget), - )) as unknown as ApplicationBuilderInternalOptions; - - buildTargetOptions.polyfills = injectTestingPolyfills(buildTargetOptions.polyfills); - - const options: KarmaBuilderOptions = { - tsConfig: unitTestOptions.tsConfig, - polyfills: buildTargetOptions.polyfills, - assets: buildTargetOptions.assets, - scripts: buildTargetOptions.scripts, - styles: buildTargetOptions.styles, - inlineStyleLanguage: buildTargetOptions.inlineStyleLanguage, - stylePreprocessorOptions: buildTargetOptions.stylePreprocessorOptions, - externalDependencies: buildTargetOptions.externalDependencies, - loader: buildTargetOptions.loader, - define: buildTargetOptions.define, - include: unitTestOptions.include, - exclude: unitTestOptions.exclude, - sourceMap: buildTargetOptions.sourceMap, - progress: buildTargetOptions.progress, - watch: unitTestOptions.watch, - poll: buildTargetOptions.poll, - preserveSymlinks: buildTargetOptions.preserveSymlinks, - browsers: unitTestOptions.browsers?.join(','), - codeCoverage: !!unitTestOptions.codeCoverage, - codeCoverageExclude: unitTestOptions.codeCoverage?.exclude, - fileReplacements: buildTargetOptions.fileReplacements, - reporters: unitTestOptions.reporters, - webWorkerTsConfig: buildTargetOptions.webWorkerTsConfig, - aot: buildTargetOptions.aot, - }; - - const { execute } = await import('../karma'); - - return execute(options, context); -} diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index c1d4b8a308a1..74b30b1e95a6 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -7,6 +7,7 @@ */ import { type BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; +import { constants, promises as fs } from 'node:fs'; import path from 'node:path'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; import { getProjectRootPaths } from '../../utils/project-metadata'; @@ -15,6 +16,26 @@ import type { Schema as UnitTestBuilderOptions } from './schema'; export type NormalizedUnitTestBuilderOptions = Awaited>; +async function exists(path: string): Promise { + try { + await fs.access(path, constants.F_OK); + + return true; + } catch { + return false; + } +} + +function normalizeReporterOption( + reporters: unknown[] | undefined, +): [string, Record][] | undefined { + return reporters?.map((entry) => + typeof entry === 'string' + ? ([entry, {}] as [string, Record]) + : (entry as [string, Record]), + ); +} + export async function normalizeOptions( context: BuilderContext, projectName: string, @@ -33,7 +54,35 @@ export async function normalizeOptions( const buildTargetSpecifier = options.buildTarget ?? `::development`; const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build'); - const { tsConfig, runner, reporters, browsers } = options; + const { runner, browsers, progress, filter, browserViewport, ui, runnerConfig } = options; + + if (ui && runner !== 'vitest') { + throw new Error('The "ui" option is only available for the "vitest" runner.'); + } + + const [width, height] = browserViewport?.split('x').map(Number) ?? []; + + let tsConfig = options.tsConfig; + if (tsConfig) { + const fullTsConfigPath = path.join(workspaceRoot, tsConfig); + if (!(await exists(fullTsConfigPath))) { + throw new Error(`The specified tsConfig file '${tsConfig}' does not exist.`); + } + } else { + const tsconfigSpecPath = path.join(projectRoot, 'tsconfig.spec.json'); + if (await exists(tsconfigSpecPath)) { + // The application builder expects a path relative to the workspace root. + tsConfig = path.relative(workspaceRoot, tsconfigSpecPath); + } + } + + let watch = options.watch ?? isTTY(); + if (options.ui && options.watch === false) { + context.logger.warn( + `The '--ui' option requires watch mode. The '--no-watch' flag will be ignored.`, + ); + watch = true; + } return { // Project/workspace information @@ -44,27 +93,41 @@ export async function normalizeOptions( // Target/configuration specified options buildTarget, include: options.include ?? ['**/*.spec.ts'], - exclude: options.exclude ?? [], - runnerName: runner, - codeCoverage: options.codeCoverage - ? { - exclude: options.codeCoverageExclude, - reporters: options.codeCoverageReporters?.map((entry) => - typeof entry === 'string' - ? ([entry, {}] as [string, Record]) - : (entry as [string, Record]), - ), - } - : undefined, + exclude: options.exclude, + filter, + runnerName: runner ?? 'vitest', + coverage: { + enabled: options.coverage, + exclude: options.coverageExclude, + include: options.coverageInclude, + reporters: normalizeReporterOption(options.coverageReporters), + thresholds: options.coverageThresholds, + // The schema generation tool doesn't support tuple types for items, but the schema validation + // does ensure that the array has exactly two numbers. + watermarks: options.coverageWatermarks as { + statements?: [number, number]; + branches?: [number, number]; + functions?: [number, number]; + lines?: [number, number]; + }, + }, tsConfig, - reporters, + buildProgress: progress, + reporters: normalizeReporterOption(options.reporters), + outputFile: options.outputFile, browsers, - watch: options.watch ?? isTTY(), + browserViewport: width && height ? { width, height } : undefined, + watch, debug: options.debug ?? false, + ui: options.ui ?? false, providersFile: options.providersFile && path.join(workspaceRoot, options.providersFile), setupFiles: options.setupFiles ? options.setupFiles.map((setupFile) => path.join(workspaceRoot, setupFile)) : [], + dumpVirtualFiles: options.dumpVirtualFiles, + listTests: options.listTests, + runnerConfig: + typeof runnerConfig === 'string' ? path.join(workspaceRoot, runnerConfig) : runnerConfig, }; } diff --git a/packages/angular/build/src/builders/unit-test/runners/api.ts b/packages/angular/build/src/builders/unit-test/runners/api.ts new file mode 100644 index 000000000000..43f65ef68adc --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/api.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import type { ApplicationBuilderInternalOptions } from '../../application/options'; +import type { FullResult, IncrementalResult } from '../../application/results'; +import type { NormalizedUnitTestBuilderOptions } from '../options'; + +/** + * Represents the options for a test runner. + */ +export interface RunnerOptions { + /** + * Partial options for the application builder. + * These will be merged with the options from the build target. + */ + buildOptions: Partial; + + /** + * A record of virtual files to be added to the build. + * The key is the file path and the value is the file content. + */ + virtualFiles?: Record; + + /** + * A map of test entry points to their corresponding test files. + * This is used to avoid re-discovering the test files in the executor. + */ + testEntryPointMappings?: Map; +} + +/** + * Represents a stateful test execution session. + * An instance of this is created for each `ng test` command. + */ +export interface TestExecutor { + /** + * Executes tests using the artifacts from a specific build. + * This method can be called multiple times in watch mode. + * + * @param buildResult The output from the application builder. + * @returns An async iterable builder output stream. + */ + execute(buildResult: FullResult | IncrementalResult): AsyncIterable; + + [Symbol.asyncDispose](): Promise; +} + +/** + * Represents the metadata and hooks for a specific test runner. + */ +export interface TestRunner { + readonly name: string; + readonly isStandalone?: boolean; + + validateDependencies?(options: NormalizedUnitTestBuilderOptions): void | Promise; + + getBuildOptions( + options: NormalizedUnitTestBuilderOptions, + baseBuildOptions: Partial, + ): RunnerOptions | Promise; + + /** + * Creates a stateful executor for a test session. + * This is called once at the start of the `ng test` command. + * + * @param context The Architect builder context. + * @param options The normalized unit test options. + * @param testEntryPointMappings A map of test entry points to their corresponding test files. + * @returns A TestExecutor instance that will handle the test runs. + */ + createExecutor( + context: BuilderContext, + options: NormalizedUnitTestBuilderOptions, + testEntryPointMappings: Map | undefined, + ): Promise; +} diff --git a/packages/angular/build/src/builders/unit-test/runners/dependency-checker.ts b/packages/angular/build/src/builders/unit-test/runners/dependency-checker.ts new file mode 100644 index 000000000000..3072bb038464 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/dependency-checker.ts @@ -0,0 +1,89 @@ +/** + * @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 { createRequire } from 'node:module'; + +/** + * A custom error class to represent missing dependency errors. + * This is used to avoid printing a stack trace for this expected error. + */ +export class MissingDependenciesError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingDependenciesError'; + } +} + +type Resolver = (packageName: string) => string; + +export class DependencyChecker { + private readonly resolver: Resolver; + private readonly missingDependencies = new Set(); + + constructor(projectSourceRoot: string) { + this.resolver = createRequire(projectSourceRoot + '/').resolve; + } + + /** + * Checks if a package is installed. + * @param packageName The name of the package to check. + * @returns True if the package is found, false otherwise. + */ + private isInstalled(packageName: string): boolean { + try { + this.resolver(packageName); + + return true; + } catch { + return false; + } + } + + /** + * Verifies that a package is installed and adds it to a list of missing + * dependencies if it is not. + * @param packageName The name of the package to check. + */ + check(packageName: string): void { + if (!this.isInstalled(packageName)) { + this.missingDependencies.add(packageName); + } + } + + /** + * Verifies that at least one of a list of packages is installed. If none are + * installed, a custom error message is added to the list of errors. + * @param packageNames An array of package names to check. + * @param customErrorMessage The error message to use if none of the packages are found. + */ + checkAny(packageNames: string[], customErrorMessage: string): void { + if (packageNames.every((name) => !this.isInstalled(name))) { + // This is a custom error, so we add it directly. + // Using a Set avoids duplicate custom messages. + this.missingDependencies.add(customErrorMessage); + } + } + + /** + * Throws a `MissingDependenciesError` if any dependencies were found to be missing. + * The error message is a formatted list of all missing packages. + */ + report(): void { + if (this.missingDependencies.size === 0) { + return; + } + + let message = 'The following packages are required but were not found:\n'; + for (const name of this.missingDependencies) { + message += ` - ${name}\n`; + } + message += 'Please install the missing packages and rerun the test command.'; + + throw new MissingDependenciesError(message); + } +} diff --git a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts new file mode 100644 index 000000000000..4b30ff0405bf --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import type { ApplicationBuilderInternalOptions } from '../../../application/options'; +import type { KarmaBuilderOptions, KarmaBuilderTransformsOptions } from '../../../karma'; +import { NormalizedUnitTestBuilderOptions } from '../../options'; +import type { TestExecutor } from '../api'; + +export class KarmaExecutor implements TestExecutor { + constructor( + private context: BuilderContext, + private options: NormalizedUnitTestBuilderOptions, + ) {} + + async *execute(): AsyncIterable { + const { context, options: unitTestOptions } = this; + + if (unitTestOptions.browserViewport) { + context.logger.warn( + 'The "karma" test runner does not support the "browserViewport" option. The option will be ignored.', + ); + } + + if (unitTestOptions.debug) { + context.logger.warn( + 'The "karma" test runner does not support the "debug" option. The option will be ignored.', + ); + } + + if (unitTestOptions.setupFiles.length) { + context.logger.warn( + 'The "karma" test runner does not support the "setupFiles" option. The option will be ignored.', + ); + } + + if (unitTestOptions.coverage?.include) { + context.logger.warn( + 'The "karma" test runner does not support the "coverageInclude" option. The option will be ignored.', + ); + } + + const buildTargetOptions = (await context.validateOptions( + await context.getTargetOptions(unitTestOptions.buildTarget), + await context.getBuilderNameForTarget(unitTestOptions.buildTarget), + )) as unknown as ApplicationBuilderInternalOptions; + + let karmaConfig: string | undefined; + if (typeof unitTestOptions.runnerConfig === 'string') { + karmaConfig = unitTestOptions.runnerConfig; + context.logger.info(`Using Karma configuration file: ${karmaConfig}`); + } else if (unitTestOptions.runnerConfig) { + const potentialPath = path.join(unitTestOptions.projectRoot, 'karma.conf.js'); + try { + await fs.access(potentialPath); + karmaConfig = potentialPath; + context.logger.info(`Using Karma configuration file: ${karmaConfig}`); + } catch { + context.logger.info('No Karma configuration file found. Using default configuration.'); + } + } + + const karmaOptions: KarmaBuilderOptions = { + karmaConfig, + tsConfig: unitTestOptions.tsConfig ?? buildTargetOptions.tsConfig, + polyfills: buildTargetOptions.polyfills, + assets: buildTargetOptions.assets, + scripts: buildTargetOptions.scripts, + styles: buildTargetOptions.styles, + inlineStyleLanguage: buildTargetOptions.inlineStyleLanguage, + stylePreprocessorOptions: buildTargetOptions.stylePreprocessorOptions, + externalDependencies: buildTargetOptions.externalDependencies, + loader: buildTargetOptions.loader, + define: buildTargetOptions.define, + include: unitTestOptions.include, + exclude: unitTestOptions.exclude, + sourceMap: buildTargetOptions.sourceMap, + progress: unitTestOptions.buildProgress ?? buildTargetOptions.progress, + watch: unitTestOptions.watch, + poll: buildTargetOptions.poll, + preserveSymlinks: buildTargetOptions.preserveSymlinks, + browsers: unitTestOptions.browsers?.join(','), + codeCoverage: unitTestOptions.coverage.enabled, + codeCoverageExclude: unitTestOptions.coverage.exclude, + fileReplacements: buildTargetOptions.fileReplacements, + reporters: unitTestOptions.reporters?.map((reporter) => { + // Karma only supports string reporters. + if (Object.keys(reporter[1]).length > 0) { + context.logger.warn( + `The "karma" test runner does not support options for the "${reporter[0]}" reporter. The options will be ignored.`, + ); + } + + return reporter[0]; + }), + webWorkerTsConfig: buildTargetOptions.webWorkerTsConfig, + aot: buildTargetOptions.aot, + }; + + const transformOptions = { + karmaOptions: (options) => { + if (unitTestOptions.filter) { + let filter = unitTestOptions.filter; + if (filter[0] === '/' && filter.at(-1) === '/') { + this.context.logger.warn( + 'The `--filter` option is always a regular expression.' + + 'Leading and trailing `/` are not required and will be ignored.', + ); + } else { + filter = `/${filter}/`; + } + + options.client ??= {}; + options.client.args ??= []; + options.client.args.push('--grep', filter); + } + + // Add coverage options + if (unitTestOptions.coverage.enabled) { + const { thresholds, watermarks } = unitTestOptions.coverage; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const coverageReporter = ((options as any).coverageReporter ??= {}); + + if (thresholds) { + coverageReporter.check = thresholds.perFile + ? { each: thresholds } + : { global: thresholds }; + } + + if (watermarks) { + coverageReporter.watermarks = watermarks; + } + } + + return options; + }, + } satisfies KarmaBuilderTransformsOptions; + + const { execute } = await import('../../../karma'); + + yield* execute(karmaOptions, context, transformOptions); + } + + async [Symbol.asyncDispose](): Promise { + // The Karma builder handles its own teardown + } +} diff --git a/packages/angular/build/src/builders/unit-test/runners/karma/index.ts b/packages/angular/build/src/builders/unit-test/runners/karma/index.ts new file mode 100644 index 000000000000..36eadb255c47 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/karma/index.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { TestRunner } from '../api'; +import { DependencyChecker } from '../dependency-checker'; +import { KarmaExecutor } from './executor'; + +/** + * A declarative definition of the Karma test runner. + */ +const KarmaTestRunner: TestRunner = { + name: 'karma', + isStandalone: true, + + validateDependencies(options) { + const checker = new DependencyChecker(options.projectSourceRoot); + checker.check('karma'); + checker.check('karma-jasmine'); + + // Check for browser launchers + if (options.browsers?.length) { + for (const browser of options.browsers) { + const launcherName = `karma-${browser.toLowerCase().split('headless')[0]}-launcher`; + checker.check(launcherName); + } + } + + if (options.coverage) { + checker.check('karma-coverage'); + } + + checker.report(); + }, + + getBuildOptions() { + return { + buildOptions: {}, + }; + }, + + async createExecutor(context, options) { + return new KarmaExecutor(context, options); + }, +}; + +export default KarmaTestRunner; diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts new file mode 100644 index 000000000000..da1827790794 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts @@ -0,0 +1,128 @@ +/** + * @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 { createRequire } from 'node:module'; +import type { BrowserBuiltinProvider, BrowserConfigOptions } from 'vitest/node'; +import { assertIsError } from '../../../../utils/error'; + +export interface BrowserConfiguration { + browser?: BrowserConfigOptions; + errors?: string[]; +} + +function findBrowserProvider( + projectResolver: NodeJS.RequireResolve, +): BrowserBuiltinProvider | undefined { + const requiresPreview = !!process.versions.webcontainer; + + // One of these must be installed in the project to use browser testing + const vitestBuiltinProviders = requiresPreview + ? (['preview'] as const) + : (['playwright', 'webdriverio', 'preview'] as const); + + for (const providerName of vitestBuiltinProviders) { + try { + projectResolver(`@vitest/browser-${providerName}`); + + return providerName; + } catch {} + } + + return undefined; +} + +function normalizeBrowserName(browserName: string): string { + // Normalize browser names to match Vitest's expectations for headless but also supports karma's names + // e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' -> 'firefox' + // and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'. + const normalized = browserName.toLowerCase(); + + return normalized.replace(/headless$/, ''); +} + +export async function setupBrowserConfiguration( + browsers: string[] | undefined, + debug: boolean, + projectSourceRoot: string, + viewport: { width: number; height: number } | undefined, +): Promise { + if (browsers === undefined) { + return {}; + } + + const projectResolver = createRequire(projectSourceRoot + '/').resolve; + let errors: string[] | undefined; + + const providerName = findBrowserProvider(projectResolver); + if (!providerName) { + errors ??= []; + errors.push( + 'The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' + + ' Please install one of these packages and rerun the test command.', + ); + } + + let provider: import('vitest/node').BrowserProviderOption | undefined; + if (providerName) { + const providerPackage = `@vitest/browser-${providerName}`; + try { + const providerModule = await import(projectResolver(providerPackage)); + + // Validate that the imported module has the expected structure + const providerFactory = providerModule[providerName]; + if (typeof providerFactory === 'function') { + provider = providerFactory(); + } else { + errors ??= []; + errors.push( + `The "${providerPackage}" package does not have a valid browser provider export.`, + ); + } + } catch (e) { + assertIsError(e); + errors ??= []; + // Check for a module not found error to provide a more specific message + if (e.code === 'ERR_MODULE_NOT_FOUND') { + errors.push( + `The "browsers" option with "${providerName}" requires the "${providerPackage}" package.` + + ' Please install this package and rerun the test command.', + ); + } else { + // Handle other potential errors during import + errors.push( + `An error occurred while loading the "${providerPackage}" browser provider:\n ${e.message}`, + ); + } + } + } + + if (errors) { + return { errors }; + } + + const isCI = !!process.env['CI']; + let headless = isCI || browsers.some((name) => name.toLowerCase().includes('headless')); + if (providerName === 'preview') { + // `preview` provider does not support headless mode + headless = false; + } + + const browser = { + enabled: true, + provider, + headless, + ui: !headless, + isolate: debug, + viewport, + instances: browsers.map((browserName) => ({ + browser: normalizeBrowserName(browserName), + })), + } satisfies BrowserConfigOptions; + + return { browser }; +} diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts new file mode 100644 index 000000000000..5df9aafe0d57 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts @@ -0,0 +1,147 @@ +/** + * @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 path from 'node:path'; +import { toPosixPath } from '../../../../utils/path'; +import type { ApplicationBuilderInternalOptions } from '../../../application/options'; +import { OutputHashing } from '../../../application/schema'; +import { NormalizedUnitTestBuilderOptions, injectTestingPolyfills } from '../../options'; +import { findTests, getTestEntrypoints } from '../../test-discovery'; +import { RunnerOptions } from '../api'; + +/** + * A list of Angular related packages that should be marked as external. + * This allows Vite to pre-bundle them, improving performance. + */ +const ANGULAR_PACKAGES_TO_EXTERNALIZE = [ + '@angular/core', + '@angular/common', + '@angular/platform-browser', + '@angular/compiler', + '@angular/router', + '@angular/forms', + '@angular/animations', + 'rxjs', +]; + +function createTestBedInitVirtualFile( + providersFile: string | undefined, + projectSourceRoot: string, + polyfills: string[] = [], +): string { + const usesZoneJS = polyfills.includes('zone.js'); + let providersImport = 'const providers = [];'; + if (providersFile) { + const relativePath = path.relative(projectSourceRoot, providersFile); + const { dir, name } = path.parse(relativePath); + const importPath = toPosixPath(path.join(dir, name)); + providersImport = `import providers from './${importPath}';`; + } + + return ` + // Initialize the Angular testing environment + import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core'; + import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing'; + import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; + ${providersImport} + // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/srcs/test_hooks.ts#L21-L29 + beforeEach(getCleanupHook(false)); + afterEach(getCleanupHook(true)); + @NgModule({ + providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}...providers], + }) + export class TestModule {} + getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }); + `; +} + +function adjustOutputHashing(hashing?: OutputHashing): OutputHashing { + switch (hashing) { + case OutputHashing.All: + case OutputHashing.Media: + // Ensure media is continued to be hashed to avoid overwriting of output media files + return OutputHashing.Media; + default: + return OutputHashing.None; + } +} + +export async function getVitestBuildOptions( + options: NormalizedUnitTestBuilderOptions, + baseBuildOptions: Partial, +): Promise { + const { workspaceRoot, projectSourceRoot, include, exclude = [], watch, providersFile } = options; + + // Find test files + const testFiles = await findTests(include, exclude, workspaceRoot, projectSourceRoot); + if (testFiles.length === 0) { + throw new Error( + 'No tests found matching the following patterns:\n' + + `- Included: ${include.join(', ')}\n` + + (exclude.length ? `- Excluded: ${exclude.join(', ')}\n` : '') + + `\nPlease check the 'test' target configuration in your project's 'angular.json' file.`, + ); + } + + const entryPoints = getTestEntrypoints(testFiles, { + projectSourceRoot, + workspaceRoot, + removeTestExtension: true, + }); + entryPoints.set('init-testbed', 'angular:test-bed-init'); + + const externalDependencies = new Set(['vitest']); + if (!options.browsers?.length) { + // Only add for non-browser setups. + // Comprehensive browser prebundling will be handled separately. + ANGULAR_PACKAGES_TO_EXTERNALIZE.forEach((dep) => externalDependencies.add(dep)); + } + if (baseBuildOptions.externalDependencies) { + baseBuildOptions.externalDependencies.forEach((dep) => externalDependencies.add(dep)); + } + + const buildOptions: Partial = { + ...baseBuildOptions, + watch, + incrementalResults: watch, + index: false, + browser: undefined, + server: undefined, + outputMode: undefined, + localize: false, + budgets: [], + serviceWorker: false, + appShell: false, + ssr: false, + prerender: false, + sourceMap: { scripts: true, vendor: false, styles: false }, + outputHashing: adjustOutputHashing(baseBuildOptions.outputHashing), + optimization: false, + entryPoints, + externalDependencies: [...externalDependencies], + }; + + buildOptions.polyfills = injectTestingPolyfills(buildOptions.polyfills); + + const testBedInitContents = createTestBedInitVirtualFile( + providersFile, + projectSourceRoot, + buildOptions.polyfills, + ); + + return { + buildOptions, + virtualFiles: { + 'angular:test-bed-init': testBedInitContents, + }, + testEntryPointMappings: entryPoints, + }; +} diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts new file mode 100644 index 000000000000..6df583350e07 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts @@ -0,0 +1,55 @@ +/** + * @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 + */ + +/** + * @fileoverview + * This file contains utility functions for finding the Vitest base configuration file. + */ + +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; + +/** + * A list of potential Vitest configuration filenames. + * The order of the files is important as the first one found will be used. + */ +const POTENTIAL_CONFIGS = [ + 'vitest-base.config.ts', + 'vitest-base.config.mts', + 'vitest-base.config.cts', + 'vitest-base.config.js', + 'vitest-base.config.mjs', + 'vitest-base.config.cjs', +]; + +/** + * Finds the Vitest configuration file in the given search directories. + * + * @param searchDirs An array of directories to search for the configuration file. + * @returns The path to the configuration file, or `false` if no file is found. + * Returning `false` is used to disable Vitest's default configuration file search. + */ +export async function findVitestBaseConfig(searchDirs: string[]): Promise { + const uniqueDirs = new Set(searchDirs); + for (const dir of uniqueDirs) { + try { + const entries = await readdir(dir, { withFileTypes: true }); + const files = new Set(entries.filter((e) => e.isFile()).map((e) => e.name)); + + for (const potential of POTENTIAL_CONFIGS) { + if (files.has(potential)) { + return path.join(dir, potential); + } + } + } catch { + // Ignore directories that cannot be read + } + } + + return false; +} diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts new file mode 100644 index 000000000000..950a96f2adac --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { BuilderOutput } from '@angular-devkit/architect'; +import assert from 'node:assert'; +import path from 'node:path'; +import { isMatch } from 'picomatch'; +import type { Vitest } from 'vitest/node'; +import { assertIsError } from '../../../../utils/error'; +import { + type FullResult, + type IncrementalResult, + type ResultFile, + ResultKind, +} from '../../../application/results'; +import { NormalizedUnitTestBuilderOptions } from '../../options'; +import type { TestExecutor } from '../api'; +import { setupBrowserConfiguration } from './browser-provider'; +import { findVitestBaseConfig } from './configuration'; +import { createVitestConfigPlugin, createVitestPlugins } from './plugins'; + +export class VitestExecutor implements TestExecutor { + private vitest: Vitest | undefined; + private normalizePath: ((id: string) => string) | undefined; + private readonly projectName: string; + private readonly options: NormalizedUnitTestBuilderOptions; + private readonly buildResultFiles = new Map(); + + // This is a reverse map of the entry points created in `build-options.ts`. + // It is used by the in-memory provider plugin to map the requested test file + // path back to its bundled output path. + // Example: `Map<'/path/to/src/app.spec.ts', 'spec-src-app-spec'>` + private readonly testFileToEntryPoint = new Map(); + private readonly entryPointToTestFile = new Map(); + + constructor( + projectName: string, + options: NormalizedUnitTestBuilderOptions, + testEntryPointMappings: Map | undefined, + ) { + this.projectName = projectName; + this.options = options; + + if (testEntryPointMappings) { + for (const [entryPoint, testFile] of testEntryPointMappings) { + this.testFileToEntryPoint.set(testFile, entryPoint); + this.entryPointToTestFile.set(entryPoint + '.js', testFile); + } + } + } + + async *execute(buildResult: FullResult | IncrementalResult): AsyncIterable { + this.normalizePath ??= (await import('vite')).normalizePath; + + if (buildResult.kind === ResultKind.Full) { + this.buildResultFiles.clear(); + for (const [path, file] of Object.entries(buildResult.files)) { + this.buildResultFiles.set(this.normalizePath(path), file); + } + } else { + for (const file of buildResult.removed) { + this.buildResultFiles.delete(this.normalizePath(file.path)); + } + for (const [path, file] of Object.entries(buildResult.files)) { + this.buildResultFiles.set(this.normalizePath(path), file); + } + } + + // Initialize Vitest if not already present. + this.vitest ??= await this.initializeVitest(); + const vitest = this.vitest; + + let testResults; + if (buildResult.kind === ResultKind.Incremental) { + // To rerun tests, Vitest needs the original test file paths, not the output paths. + const modifiedSourceFiles = new Set(); + for (const modifiedFile of buildResult.modified) { + // The `modified` files in the build result are the output paths. + // We need to find the original source file path to pass to Vitest. + const source = this.entryPointToTestFile.get(modifiedFile); + if (source) { + modifiedSourceFiles.add(source); + } + vitest.invalidateFile( + this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)), + ); + } + + const specsToRerun = []; + for (const file of modifiedSourceFiles) { + vitest.invalidateFile(file); + const specs = vitest.getModuleSpecifications(file); + if (specs) { + specsToRerun.push(...specs); + } + } + + if (specsToRerun.length > 0) { + testResults = await vitest.rerunTestSpecifications(specsToRerun); + } + } + + // Check if all the tests pass to calculate the result + const testModules = testResults?.testModules ?? this.vitest.state.getTestModules(); + + yield { success: testModules.every((testModule) => testModule.ok()) }; + } + + async [Symbol.asyncDispose](): Promise { + await this.vitest?.close(); + } + + private prepareSetupFiles(): string[] { + const { setupFiles } = this.options; + // Add setup file entries for TestBed initialization and project polyfills + const testSetupFiles = ['init-testbed.js', ...setupFiles]; + + // TODO: Provide additional result metadata to avoid needing to extract based on filename + if (this.buildResultFiles.has('polyfills.js')) { + testSetupFiles.unshift('polyfills.js'); + } + + return testSetupFiles; + } + + private async initializeVitest(): Promise { + const { + coverage, + reporters, + outputFile, + workspaceRoot, + browsers, + debug, + watch, + browserViewport, + ui, + } = this.options; + const projectName = this.projectName; + + let vitestNodeModule; + try { + vitestNodeModule = await import('vitest/node'); + } catch (error: unknown) { + assertIsError(error); + if (error.code !== 'ERR_MODULE_NOT_FOUND') { + throw error; + } + throw new Error( + 'The `vitest` package was not found. Please install the package and rerun the test command.', + ); + } + const { startVitest } = vitestNodeModule; + + // Setup vitest browser options if configured + const browserOptions = await setupBrowserConfiguration( + browsers, + debug, + this.options.projectSourceRoot, + browserViewport, + ); + if (browserOptions.errors?.length) { + throw new Error(browserOptions.errors.join('\n')); + } + + assert( + this.buildResultFiles.size > 0, + 'buildResult must be available before initializing vitest', + ); + + const testSetupFiles = this.prepareSetupFiles(); + const projectPlugins = createVitestPlugins({ + workspaceRoot, + projectSourceRoot: this.options.projectSourceRoot, + projectName, + buildResultFiles: this.buildResultFiles, + testFileToEntryPoint: this.testFileToEntryPoint, + }); + + const debugOptions = debug + ? { + inspectBrk: true, + isolate: false, + fileParallelism: false, + } + : {}; + + const runnerConfig = this.options.runnerConfig; + const externalConfigPath = + runnerConfig === true + ? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot]) + : runnerConfig; + + return startVitest( + 'test', + undefined, + { + config: externalConfigPath, + root: workspaceRoot, + project: projectName, + outputFile, + testNamePattern: this.options.filter, + watch, + ui, + ...debugOptions, + }, + { + server: { + // Disable the actual file watcher. The boolean watch option above should still + // be enabled as it controls other internal behavior related to rerunning tests. + watch: null, + }, + plugins: [ + createVitestConfigPlugin({ + browser: browserOptions.browser, + coverage, + projectName, + projectSourceRoot: this.options.projectSourceRoot, + reporters, + setupFiles: testSetupFiles, + projectPlugins, + include: [...this.testFileToEntryPoint.keys()], + }), + ], + }, + ); + } +} diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts new file mode 100644 index 000000000000..fed814bdd78e --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts @@ -0,0 +1,74 @@ +/** + * @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 assert from 'node:assert'; +import type { TestRunner } from '../api'; +import { DependencyChecker } from '../dependency-checker'; +import { getVitestBuildOptions } from './build-options'; +import { VitestExecutor } from './executor'; + +/** + * A declarative definition of the Vitest test runner. + */ +const VitestTestRunner: TestRunner = { + name: 'vitest', + + validateDependencies(options) { + const checker = new DependencyChecker(options.projectSourceRoot); + checker.check('vitest'); + + if (options.browsers?.length) { + if (process.versions.webcontainer) { + checker.check('@vitest/browser-preview'); + } else { + checker.checkAny( + ['@vitest/browser-playwright', '@vitest/browser-webdriverio', '@vitest/browser-preview'], + 'The "browsers" option requires either ' + + '"@vitest/browser-playwright", "@vitest/browser-webdriverio", or "@vitest/browser-preview" to be installed.', + ); + } + } else { + // DOM emulation is used when no browsers are specified + checker.checkAny( + ['jsdom', 'happy-dom'], + 'A DOM environment is required for non-browser tests. Please install either "jsdom" or "happy-dom".', + ); + } + + if (options.coverage.enabled) { + checker.check('@vitest/coverage-v8'); + } + + checker.report(); + }, + + getBuildOptions(options, baseBuildOptions) { + return getVitestBuildOptions(options, baseBuildOptions); + }, + + async createExecutor(context, options, testEntryPointMappings) { + const projectName = context.target?.project; + assert(projectName, 'The builder requires a target.'); + + if (!!process.versions.webcontainer && options.browsers?.length) { + context.logger.info( + `Webcontainer environment detected. Using '@vitest/browser-preview' for browser-based tests.`, + ); + } + + if (typeof options.runnerConfig === 'string') { + context.logger.info(`Using Vitest configuration file: ${options.runnerConfig}`); + } else if (options.runnerConfig) { + context.logger.info('Automatically searching for and using Vitest configuration file.'); + } + + return new VitestExecutor(projectName, options, testEntryPointMappings); + }, +}; + +export default VitestTestRunner; diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts new file mode 100644 index 000000000000..e425bdf95e4d --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -0,0 +1,294 @@ +/** + * @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 assert from 'node:assert'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import type { + BrowserConfigOptions, + InlineConfig, + UserWorkspaceConfig, + VitestPlugin, +} from 'vitest/node'; +import { createBuildAssetsMiddleware } from '../../../../tools/vite/middlewares/assets-middleware'; +import { toPosixPath } from '../../../../utils/path'; +import type { ResultFile } from '../../../application/results'; +import type { NormalizedUnitTestBuilderOptions } from '../../options'; + +type VitestPlugins = Awaited>; + +interface PluginOptions { + workspaceRoot: string; + projectSourceRoot: string; + projectName: string; + buildResultFiles: ReadonlyMap; + testFileToEntryPoint: ReadonlyMap; +} + +type VitestCoverageOption = Exclude; + +interface VitestConfigPluginOptions { + browser: BrowserConfigOptions | undefined; + coverage: NormalizedUnitTestBuilderOptions['coverage']; + projectName: string; + projectSourceRoot: string; + reporters?: string[] | [string, object][]; + setupFiles: string[]; + projectPlugins: Exclude; + include: string[]; +} + +async function findTestEnvironment( + projectResolver: NodeJS.RequireResolve, +): Promise<'jsdom' | 'happy-dom'> { + try { + projectResolver('happy-dom'); + + return 'happy-dom'; + } catch { + // happy-dom is not installed, fallback to jsdom + return 'jsdom'; + } +} + +export function createVitestConfigPlugin(options: VitestConfigPluginOptions): VitestPlugins[0] { + const { + include, + browser, + projectName, + reporters, + setupFiles, + projectPlugins, + projectSourceRoot, + } = options; + + return { + name: 'angular:vitest-configuration', + async config(config) { + const testConfig = config.test; + + if (testConfig?.projects?.length) { + this.warn( + 'The "test.projects" option in the Vitest configuration file is not supported. ' + + 'The Angular CLI Test system will construct its own project configuration.', + ); + delete testConfig.projects; + } + + if (testConfig?.include) { + this.warn( + 'The "test.include" option in the Vitest configuration file is not supported. ' + + 'The Angular CLI Test system will manage test file discovery.', + ); + delete testConfig.include; + } + + // The user's setup files should be appended to the CLI's setup files. + const combinedSetupFiles = [...setupFiles]; + if (testConfig?.setupFiles) { + if (typeof testConfig.setupFiles === 'string') { + combinedSetupFiles.push(testConfig.setupFiles); + } else if (Array.isArray(testConfig.setupFiles)) { + combinedSetupFiles.push(...testConfig.setupFiles); + } + delete testConfig.setupFiles; + } + + // Merge user-defined plugins from the Vitest config with the CLI's internal plugins. + if (config.plugins) { + const userPlugins = config.plugins.filter( + (plugin) => + // Only inspect objects with a `name` property as these would be the internal injected plugins + !plugin || + typeof plugin !== 'object' || + !('name' in plugin) || + (!plugin.name.startsWith('angular:') && !plugin.name.startsWith('vitest')), + ); + + if (userPlugins.length > 0) { + projectPlugins.push(...userPlugins); + } + } + + const projectResolver = createRequire(projectSourceRoot + '/').resolve; + + const projectConfig: UserWorkspaceConfig = { + test: { + ...testConfig, + name: projectName, + setupFiles: combinedSetupFiles, + include, + globals: testConfig?.globals ?? true, + ...(browser ? { browser } : {}), + // If the user has not specified an environment, use a smart default. + ...(!testConfig?.environment + ? { environment: await findTestEnvironment(projectResolver) } + : {}), + }, + optimizeDeps: { + noDiscovery: true, + }, + plugins: projectPlugins, + }; + + return { + test: { + coverage: await generateCoverageOption(options.coverage, projectName), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(reporters ? ({ reporters } as any) : {}), + ...(browser ? { browser } : {}), + projects: [projectConfig], + }, + }; + }, + }; +} + +export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins { + const { workspaceRoot, buildResultFiles, testFileToEntryPoint } = pluginOptions; + + return [ + { + name: 'angular:test-in-memory-provider', + enforce: 'pre', + resolveId: (id, importer) => { + if (importer && (id[0] === '.' || id[0] === '/')) { + let fullPath; + if (testFileToEntryPoint.has(importer)) { + fullPath = toPosixPath(path.join(workspaceRoot, id)); + } else { + fullPath = toPosixPath(path.join(path.dirname(importer), id)); + } + + const relativePath = path.relative(workspaceRoot, fullPath); + if (buildResultFiles.has(toPosixPath(relativePath))) { + return fullPath; + } + } + + if (testFileToEntryPoint.has(id)) { + return id; + } + + assert(buildResultFiles.size > 0, 'buildResult must be available for resolving.'); + const relativePath = path.relative(workspaceRoot, id); + if (buildResultFiles.has(toPosixPath(relativePath))) { + return id; + } + }, + load: async (id) => { + assert(buildResultFiles.size > 0, 'buildResult must be available for in-memory loading.'); + + // Attempt to load as a source test file. + const entryPoint = testFileToEntryPoint.get(id); + let outputPath; + if (entryPoint) { + outputPath = entryPoint + '.js'; + + // To support coverage exclusion of the actual test file, the virtual + // test entry point only references the built and bundled intermediate file. + return { + code: `import "./${outputPath}";`, + }; + } else { + // Attempt to load as a built artifact. + const relativePath = path.relative(workspaceRoot, id); + outputPath = toPosixPath(relativePath); + } + + const outputFile = buildResultFiles.get(outputPath); + if (outputFile) { + const sourceMapPath = outputPath + '.map'; + const sourceMapFile = buildResultFiles.get(sourceMapPath); + const code = + outputFile.origin === 'memory' + ? Buffer.from(outputFile.contents).toString('utf-8') + : await readFile(outputFile.inputPath, 'utf-8'); + const sourceMapText = sourceMapFile + ? sourceMapFile.origin === 'memory' + ? Buffer.from(sourceMapFile.contents).toString('utf-8') + : await readFile(sourceMapFile.inputPath, 'utf-8') + : undefined; + + // Vitest will include files in the coverage report if the sourcemap contains no sources. + // For builder-internal generated code chunks, which are typically helper functions, + // a virtual source is added to the sourcemap to prevent them from being incorrectly + // included in the final coverage report. + const map = sourceMapText ? JSON.parse(sourceMapText) : undefined; + if (map) { + if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) { + map.sources = ['virtual:builder']; + } + } + + return { + code, + map, + }; + } + }, + configureServer: (server) => { + server.middlewares.use(createBuildAssetsMiddleware(server.config.base, buildResultFiles)); + }, + }, + { + name: 'angular:html-index', + transformIndexHtml: () => { + // Add all global stylesheets + if (buildResultFiles.has('styles.css')) { + return [ + { + tag: 'link', + attrs: { href: 'styles.css', rel: 'stylesheet' }, + injectTo: 'head', + }, + ]; + } + + return []; + }, + }, + ]; +} + +async function generateCoverageOption( + coverage: NormalizedUnitTestBuilderOptions['coverage'], + projectName: string, +): Promise { + let defaultExcludes: string[] = []; + if (coverage.exclude) { + try { + const vitestConfig = await import('vitest/config'); + defaultExcludes = vitestConfig.coverageConfigDefaults.exclude; + } catch {} + } + + return { + enabled: coverage.enabled, + excludeAfterRemap: true, + include: coverage.include, + reportsDirectory: toPosixPath(path.join('coverage', projectName)), + thresholds: coverage.thresholds, + watermarks: coverage.watermarks, + // Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures + ...(coverage.exclude + ? { + exclude: [ + // Augment the default exclude https://vitest.dev/config/#coverage-exclude + // with the user defined exclusions + ...coverage.exclude, + ...defaultExcludes, + ], + } + : {}), + ...(coverage.reporters + ? ({ reporter: coverage.reporters } satisfies VitestCoverageOption) + : {}), + }; +} diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 8628bb9725e9..ed766c1774a2 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -6,71 +6,105 @@ "properties": { "buildTarget": { "type": "string", - "description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "description": "Specifies the build target to use for the unit test build in the format `project:target[:configuration]`. This defaults to the `build` target of the current project with the `development` configuration. You can also pass a comma-separated list of configurations. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$" }, "tsConfig": { "type": "string", - "description": "The name of the TypeScript configuration file." + "description": "The path to the TypeScript configuration file, relative to the workspace root. Defaults to `tsconfig.spec.json` in the project root if it exists. If not specified and the default does not exist, the `tsConfig` from the specified `buildTarget` will be used." }, "runner": { "type": "string", - "description": "The name of the test runner to use for test execution.", + "description": "Specifies the test runner to use for test execution.", + "default": "vitest", "enum": ["karma", "vitest"] }, + "runnerConfig": { + "type": ["boolean", "string"], + "description": "Specifies the configuration file for the selected test runner. If a string is provided, it will be used as the path to the configuration file. If `true`, the builder will search for a default configuration file (e.g., `vitest.config.ts` or `karma.conf.js`). If `false`, no external configuration file will be used.\\nFor Vitest, this enables advanced options and the use of custom plugins. Please note that while the file is loaded, the Angular team does not provide direct support for its specific contents or any third-party plugins used within it.", + "default": false + }, "browsers": { - "description": "A list of browsers to use for test execution. If undefined, jsdom on Node.js will be used instead of a browser. For Vitest and Karma, browser names ending with 'Headless' (e.g., 'ChromeHeadless') will enable headless mode for that browser.", + "description": "Specifies the browsers to use for test execution. When not specified, tests are run in a Node.js environment using jsdom. For both Vitest and Karma, browser names ending with 'Headless' (e.g., 'ChromeHeadless') will enable headless mode.", "type": "array", "items": { "type": "string" }, "minItems": 1 }, + "browserViewport": { + "description": "Specifies the browser viewport dimensions for browser-based tests in the format `widthxheight`.", + "type": "string", + "pattern": "^\\d+x\\d+$" + }, "include": { "type": "array", "items": { "type": "string" }, - "default": ["**/*.spec.ts"], - "description": "Globs of files to include, relative to project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead." + "default": ["**/*.spec.ts", "**/*.test.ts"], + "description": "Specifies glob patterns of files to include for testing, relative to the project root. This option also has special handling for directory paths (includes all test files within) and file paths (includes the corresponding test file if one exists)." }, "exclude": { "type": "array", "items": { "type": "string" }, - "default": [], - "description": "Globs of files to exclude, relative to the project root." + "description": "Specifies glob patterns of files to exclude from testing, relative to the project root." + }, + "filter": { + "type": "string", + "description": "Specifies a regular expression pattern to match against test suite and test names. Only tests with a name matching the pattern will be executed. For example, `^App` will run only tests in suites beginning with 'App'." }, "watch": { "type": "boolean", - "description": "Re-run tests when source files change. Defaults to `true` in TTY environments and `false` otherwise." + "description": "Enables watch mode, which re-runs tests when source files change. Defaults to `true` in TTY environments and `false` otherwise." }, "debug": { "type": "boolean", - "description": "Initialize the test runner to support using the Node Inspector for test debugging.", + "description": "Enables debugging mode for tests, allowing the use of the Node Inspector.", + "default": false + }, + "ui": { + "type": "boolean", + "description": "Enables the Vitest UI for interactive test execution. This option is only available for the Vitest runner.", "default": false }, - "codeCoverage": { + "coverage": { "type": "boolean", - "description": "Output a code coverage report.", + "description": "Enables coverage reporting for tests.", "default": false }, - "codeCoverageExclude": { + "coverageInclude": { "type": "array", - "description": "Globs to exclude from code coverage.", + "description": "Specifies glob patterns of files to include in the coverage report.", "items": { "type": "string" - }, - "default": [] + } }, - "codeCoverageReporters": { + "coverageExclude": { "type": "array", - "description": "Reporters to use for code coverage results.", + "description": "Specifies glob patterns of files to exclude from the coverage report.", + "items": { + "type": "string" + } + }, + "coverageReporters": { + "type": "array", + "description": "Specifies the reporters to use for coverage results. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'html', 'lcov', 'lcovonly', 'text', 'text-summary', 'cobertura', 'json', and 'json-summary'.", "items": { "oneOf": [ { - "$ref": "#/definitions/coverage-reporters" + "enum": [ + "html", + "lcov", + "lcovonly", + "text", + "text-summary", + "cobertura", + "json", + "json-summary" + ] }, { "type": "array", @@ -78,7 +112,16 @@ "maxItems": 2, "items": [ { - "$ref": "#/definitions/coverage-reporters" + "enum": [ + "html", + "lcov", + "lcovonly", + "text", + "text-summary", + "cobertura", + "json", + "json-summary" + ] }, { "type": "object" @@ -88,16 +131,122 @@ ] } }, + "coverageThresholds": { + "type": "object", + "description": "Specifies minimum coverage thresholds that must be met. If thresholds are not met, the builder will exit with an error.", + "properties": { + "perFile": { + "type": "boolean", + "description": "When true, thresholds are enforced for each file individually." + }, + "statements": { + "type": "number", + "description": "Minimum percentage of statements covered." + }, + "branches": { + "type": "number", + "description": "Minimum percentage of branches covered." + }, + "functions": { + "type": "number", + "description": "Minimum percentage of functions covered." + }, + "lines": { + "type": "number", + "description": "Minimum percentage of lines covered." + } + }, + "additionalProperties": false + }, + "coverageWatermarks": { + "type": "object", + "description": "Specifies coverage watermarks for the HTML reporter. These determine the color coding for high, medium, and low coverage.", + "properties": { + "statements": { + "type": "array", + "description": "The high and low watermarks for statements coverage. `[low, high]`", + "items": { "type": "number" }, + "minItems": 2, + "maxItems": 2 + }, + "branches": { + "type": "array", + "description": "The high and low watermarks for branches coverage. `[low, high]`", + "items": { "type": "number" }, + "minItems": 2, + "maxItems": 2 + }, + "functions": { + "type": "array", + "description": "The high and low watermarks for functions coverage. `[low, high]`", + "items": { "type": "number" }, + "minItems": 2, + "maxItems": 2 + }, + "lines": { + "type": "array", + "description": "The high and low watermarks for lines coverage. `[low, high]`", + "items": { "type": "number" }, + "minItems": 2, + "maxItems": 2 + } + }, + "additionalProperties": false + }, "reporters": { "type": "array", - "description": "Test runner reporters to use. Directly passed to the test runner.", + "description": "Specifies the reporters to use during test execution. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'default', 'verbose', 'dots', 'json', 'junit', 'tap', 'tap-flat', and 'html'. You can also provide a path to a custom reporter.", "items": { - "type": "string" + "oneOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["default", "verbose", "dots", "json", "junit", "tap", "tap-flat", "html"] + } + ] + }, + { + "type": "array", + "minItems": 1, + "maxItems": 2, + "items": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + "default", + "verbose", + "dots", + "json", + "junit", + "tap", + "tap-flat", + "html" + ] + } + ] + }, + { + "type": "object" + } + ] + } + ] } }, + "outputFile": { + "type": "string", + "description": "Specifies a file path for the test report, applying only to the first reporter. To configure output files for multiple reporters, use the tuple format `['reporter-name', { outputFile: '...' }]` within the `reporters` option. When not provided, output is written to the console." + }, "providersFile": { "type": "string", - "description": "TypeScript file that exports an array of Angular providers to use during test execution. The array must be a default export.", + "description": "Specifies the path to a TypeScript file that provides an array of Angular providers for the test environment. The file must contain a default export of the provider array.", "minLength": 1 }, "setupFiles": { @@ -105,23 +254,24 @@ "items": { "type": "string" }, - "description": "A list of global setup and configuration files that are included before the test files. The application's polyfills are always included before these files. The Angular Testbed is also initialized prior to the execution of these files." + "description": "A list of paths to global setup files that are executed before the test files. The application's polyfills and the Angular TestBed are always initialized before these files." + }, + "progress": { + "type": "boolean", + "description": "Shows build progress information in the console. Defaults to the `progress` setting of the specified `buildTarget`." + }, + "listTests": { + "type": "boolean", + "description": "Lists all discovered test files and exits the process without building or executing the tests.", + "default": false + }, + "dumpVirtualFiles": { + "type": "boolean", + "description": "Dumps build output files to the `.angular/cache` directory for debugging purposes.", + "default": false, + "visible": false } }, "additionalProperties": false, - "required": ["buildTarget", "tsConfig", "runner"], - "definitions": { - "coverage-reporters": { - "enum": [ - "html", - "lcov", - "lcovonly", - "text", - "text-summary", - "cobertura", - "json", - "json-summary" - ] - } - } + "required": [] } diff --git a/packages/angular/build/src/builders/unit-test/test-discovery.ts b/packages/angular/build/src/builders/unit-test/test-discovery.ts new file mode 100644 index 000000000000..89d38dbbe787 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/test-discovery.ts @@ -0,0 +1,295 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { type PathLike, constants, promises as fs } from 'node:fs'; +import os from 'node:os'; +import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path'; +import { glob, isDynamicPattern } from 'tinyglobby'; +import { toPosixPath } from '../../utils/path'; + +/** + * An array of file infix notations that identify a file as a test file. + * For example, `.spec` in `app.component.spec.ts`. + */ +const TEST_FILE_INFIXES = ['.spec', '.test']; + +/** + * Finds all test files in the project. This function implements a special handling + * for static paths (non-globs) to improve developer experience. For example, if a + * user provides a path to a component, this function will find the corresponding + * test file. If a user provides a path to a directory, it will find all test + * files within that directory. + * + * @param include Glob patterns of files to include. + * @param exclude Glob patterns of files to exclude. + * @param workspaceRoot The absolute path to the workspace root. + * @param projectSourceRoot The absolute path to the project's source root. + * @returns A unique set of absolute paths to all test files. + */ +export async function findTests( + include: string[], + exclude: string[], + workspaceRoot: string, + projectSourceRoot: string, +): Promise { + const resolvedTestFiles = new Set(); + const dynamicPatterns: string[] = []; + + const projectRootPrefix = toPosixPath(relative(workspaceRoot, projectSourceRoot) + '/'); + const normalizedExcludes = exclude.map((p) => normalizePattern(p, projectRootPrefix)); + + // 1. Separate static and dynamic patterns + for (const pattern of include) { + const normalized = normalizePattern(pattern, projectRootPrefix); + if (isDynamicPattern(pattern)) { + dynamicPatterns.push(normalized); + } else { + const { resolved, unresolved } = await resolveStaticPattern(normalized, projectSourceRoot); + resolved.forEach((file) => resolvedTestFiles.add(file)); + unresolved.forEach((p) => dynamicPatterns.push(p)); + } + } + + // 2. Execute a single glob for all dynamic patterns + if (dynamicPatterns.length > 0) { + const globMatches = await glob(dynamicPatterns, { + cwd: projectSourceRoot, + absolute: true, + expandDirectories: false, + ignore: ['**/node_modules/**', ...normalizedExcludes], + }); + + for (const match of globMatches) { + resolvedTestFiles.add(match); + } + } + + // 3. Combine and de-duplicate results + return [...resolvedTestFiles]; +} + +interface TestEntrypointsOptions { + projectSourceRoot: string; + workspaceRoot: string; + removeTestExtension?: boolean; +} + +/** + * Generates unique, dash-delimited bundle names for a set of test files. + * This is used to create distinct output files for each test. + * + * @param testFiles An array of absolute paths to test files. + * @param options Configuration options for generating entry points. + * @returns A map where keys are the generated unique bundle names and values are the original file paths. + */ +export function getTestEntrypoints( + testFiles: string[], + { projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions, +): Map { + const seen = new Set(); + const roots = [projectSourceRoot, workspaceRoot]; + + return new Map( + Array.from(testFiles, (testFile) => { + const fileName = generateNameFromPath(testFile, roots, !!removeTestExtension); + const baseName = `spec-${fileName}`; + let uniqueName = baseName; + let suffix = 2; + while (seen.has(uniqueName)) { + uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1'); + ++suffix; + } + seen.add(uniqueName); + + return [uniqueName, testFile]; + }), + ); +} + +/** + * Generates a unique, dash-delimited name from a file path. This is used to + * create a consistent and readable bundle name for a given test file. + * + * @param testFile The absolute path to the test file. + * @param roots An array of root paths to remove from the beginning of the test file path. + * @param removeTestExtension Whether to remove the test file infix and extension from the result. + * @returns A dash-cased name derived from the relative path of the test file. + */ +function generateNameFromPath( + testFile: string, + roots: string[], + removeTestExtension: boolean, +): string { + const relativePath = removeRoots(testFile, roots); + + let startIndex = 0; + // Skip leading dots and slashes + while (startIndex < relativePath.length && /^[./\\]$/.test(relativePath[startIndex])) { + startIndex++; + } + + let endIndex = relativePath.length; + if (removeTestExtension) { + const infixes = TEST_FILE_INFIXES.map((p) => p.substring(1)).join('|'); + const match = relativePath.match(new RegExp(`\\.(${infixes})\\.[^.]+$`)); + + if (match?.index) { + endIndex = match.index; + } + } else { + const extIndex = relativePath.lastIndexOf('.'); + if (extIndex > startIndex) { + endIndex = extIndex; + } + } + + // Build the final string in a single pass + let result = ''; + for (let i = startIndex; i < endIndex; i++) { + const char = relativePath[i]; + result += char === '/' || char === '\\' ? '-' : char; + } + + return result; +} + +/** + * Whether the current operating system's filesystem is case-insensitive. + */ +const isCaseInsensitiveFilesystem = os.platform() === 'win32' || os.platform() === 'darwin'; + +/** + * Removes a prefix from the beginning of a string, with conditional case-insensitivity + * based on the operating system's filesystem characteristics. + * + * @param text The string to remove the prefix from. + * @param prefix The prefix to remove. + * @returns The string with the prefix removed, or the original string if the prefix was not found. + */ +function removePrefix(text: string, prefix: string): string { + if (isCaseInsensitiveFilesystem) { + if (text.toLowerCase().startsWith(prefix.toLowerCase())) { + return text.substring(prefix.length); + } + } else { + if (text.startsWith(prefix)) { + return text.substring(prefix.length); + } + } + + return text; +} + +/** + * Removes potential root paths from a file path, returning a relative path. + * If no root path matches, it returns the file's basename. + * + * @param path The file path to process. + * @param roots An array of root paths to attempt to remove. + * @returns A relative path. + */ +function removeRoots(path: string, roots: string[]): string { + for (const root of roots) { + const result = removePrefix(path, root); + // If the prefix was removed, the result will be a different string. + if (result !== path) { + return result; + } + } + + return basename(path); +} + +/** + * Normalizes a glob pattern by converting it to a POSIX path, removing leading + * slashes, and making it relative to the project source root. + * + * @param pattern The glob pattern to normalize. + * @param projectRootPrefix The POSIX-formatted prefix of the project's source root relative to the workspace root. + * @returns A normalized glob pattern. + */ +function normalizePattern(pattern: string, projectRootPrefix: string): string { + const posixPattern = toPosixPath(pattern); + + // Do not modify absolute paths. The globber will handle them correctly. + if (isAbsolute(posixPattern)) { + return posixPattern; + } + + // For relative paths, ensure they are correctly relative to the project source root. + // This involves removing the project root prefix if the user provided a workspace-relative path. + const normalizedRelative = removePrefix(posixPattern, projectRootPrefix); + + return normalizedRelative; +} + +/** + * Resolves a static (non-glob) path. + * + * If the path is a directory, it returns a glob pattern to find all test files + * within that directory. + * + * If the path is a file, it attempts to find a corresponding test file by + * checking for files with the same name and a test infix (e.g., `.spec.ts`). + * + * If no corresponding test file is found, the original path is returned as an + * unresolved pattern. + * + * @param pattern The static path pattern. + * @param projectSourceRoot The absolute path to the project's source root. + * @returns A promise that resolves to an object containing resolved spec files and unresolved patterns. + */ +async function resolveStaticPattern( + pattern: string, + projectSourceRoot: string, +): Promise<{ resolved: string[]; unresolved: string[] }> { + const fullPath = isAbsolute(pattern) ? pattern : join(projectSourceRoot, pattern); + if (await isDirectory(fullPath)) { + const infixes = TEST_FILE_INFIXES.map((p) => p.substring(1)).join('|'); + + return { resolved: [], unresolved: [`${pattern}/**/*.@(${infixes}).@(ts|tsx)`] }; + } + + const fileExt = extname(fullPath); + const baseName = basename(fullPath, fileExt); + + for (const infix of TEST_FILE_INFIXES) { + const potentialSpec = join(dirname(fullPath), `${baseName}${infix}${fileExt}`); + if (await exists(potentialSpec)) { + return { resolved: [potentialSpec], unresolved: [] }; + } + } + + if (await exists(fullPath)) { + return { resolved: [fullPath], unresolved: [] }; + } + + return { resolved: [], unresolved: [pattern] }; +} + +/** Checks if a path exists and is a directory. */ +async function isDirectory(path: PathLike): Promise { + try { + const stats = await fs.stat(path); + + return stats.isDirectory(); + } catch { + return false; + } +} + +/** Checks if a path exists on the file system. */ +async function exists(path: PathLike): Promise { + try { + await fs.access(path, constants.F_OK); + + return true; + } catch { + return false; + } +} diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts new file mode 100644 index 000000000000..ec3a52e7686b --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts @@ -0,0 +1,321 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../index'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +const VITEST_CONFIG_CONTENT = ` +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + test: { + reporters: [['junit', { outputFile: './vitest-results.xml' }]], + }, +}); +`; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Behavior: "runnerConfig with Vitest runner"', () => { + beforeEach(() => { + setupApplicationTarget(harness); + }); + + it('should use custom reporters defined in runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should exclude test files based on runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + exclude: ['src/app/app.component.spec.ts'], + reporters: ['default', ['json', { outputFile: 'vitest-results.json' }]], + }, + }); + `, + ); + + // Create a second test file that should be executed + harness.writeFile( + 'src/app/app-second.spec.ts', + ` + import { TestBed } from '@angular/core/testing'; + import { AppComponent } from './app.component'; + + describe('AppComponent', () => { + beforeEach(() => TestBed.configureTestingModule({ + declarations: [AppComponent], + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const results = JSON.parse(harness.readFile('vitest-results.json')); + expect(results.numPassedTests).toBe(1); + }); + + it('should allow overriding builder options via runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + globals: false, + }, + }); + `, + ); + + // This test will fail if globals are enabled, because `test` will not be defined. + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { vi, test, expect } from 'vitest'; + test('should pass', () => { + expect(true).toBe(true); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeFalse(); + }); + + it('should fail when a DOM-dependent test is run in a node environment', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + environment: 'node', + }, + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeFalse(); + }); + + it('should warn and ignore "test.projects" option from runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + projects: ['./foo.config.ts'], + }, + }); + `, + ); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // TODO: Re-enable once Vite logs are remapped through build system + // expect(logs).toContain( + // jasmine.objectContaining({ + // level: 'warn', + // message: jasmine.stringMatching( + // 'The "test.projects" option in the Vitest configuration file is not supported.', + // ), + // }), + // ); + }); + + it('should warn and ignore "test.include" option from runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + include: ['src/app/non-existent.spec.ts'], + }, + }); + `, + ); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // TODO: Re-enable once Vite logs are remapped through build system + // expect(logs).toContain( + // jasmine.objectContaining({ + // level: 'warn', + // message: jasmine.stringMatching( + // 'The "test.include" option in the Vitest configuration file is not supported.', + // ), + // }), + // ); + }); + + it(`should append "test.setupFiles" (string) from runnerConfig to the CLI's setup`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + setupFiles: './src/app/custom-setup.ts', + }, + }); + `, + ); + + harness.writeFile('src/app/custom-setup.ts', `(globalThis as any).customSetupLoaded = true;`); + + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + test('should have custom setup loaded', () => { + expect((globalThis as any).customSetupLoaded).toBe(true); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it(`should append "test.setupFiles" (array) from runnerConfig to the CLI's setup`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + setupFiles: ['./src/app/custom-setup-1.ts', './src/app/custom-setup-2.ts'], + }, + }); + `, + ); + + harness.writeFile('src/app/custom-setup-1.ts', `(globalThis as any).customSetup1 = true;`); + harness.writeFile('src/app/custom-setup-2.ts', `(globalThis as any).customSetup2 = true;`); + + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + test('should have custom setups loaded', () => { + expect((globalThis as any).customSetup1).toBe(true); + expect((globalThis as any).customSetup2).toBe(true); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it('should merge and apply custom Vite plugins from runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + plugins: [ + { + name: 'my-custom-transform-plugin', + transform(code, id) { + if (code.includes('__PLACEHOLDER__')) { + return code.replace('__PLACEHOLDER__', 'transformed by custom plugin'); + } + }, + }, + ], + }); + `, + ); + + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + test('should have been transformed by custom plugin', () => { + const placeholder = '__PLACEHOLDER__'; + expect(placeholder).toBe('transformed by custom plugin'); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); +}); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/browsers_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/browsers_spec.ts index 1acc1ee2924a..aa0b3c4368fa 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/browsers_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/browsers_spec.ts @@ -12,6 +12,7 @@ import { describeBuilder, UNIT_TEST_BUILDER_INFO, setupApplicationTarget, + expectLog, } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { @@ -28,9 +29,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ message: 'Using jsdom in Node.js for test execution.' }), - ); + expectLog(logs, 'Using jsdom in Node.js for test execution.'); }); it('should fail when browsers is empty', async () => { @@ -52,9 +51,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/Starting browser "chrome"/) }), - ); + expectLog(logs, /Starting browser "chrome"/); }); it('should launch a browser in headless mode when specified', async () => { @@ -65,11 +62,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/Starting browser "chrome" in headless mode/), - }), - ); + expectLog(logs, /Starting browser "chrome" in headless mode/); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-exclude_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-exclude_spec.ts index 99a730504bbc..b92e709d2f12 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-exclude_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-exclude_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "codeCoverageExclude"', () => { + describe('Option: "coverageExclude"', () => { beforeEach(async () => { setupApplicationTarget(harness); await harness.writeFiles({ @@ -26,26 +26,28 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { it('should not exclude any files from coverage when not provided', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - codeCoverage: true, + coverage: true, + coverageInclude: ['**/*.ts'], }); const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - const summary = harness.readFile('coverage/coverage-summary.json'); - expect(summary).toContain('"src/app/error.ts"'); + const summary = harness.readFile('coverage/test/coverage-final.json'); + expect(summary).toContain('src/app/error.ts"'); }); it('should exclude files from coverage that match the glob pattern', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - codeCoverage: true, - codeCoverageExclude: ['**/error.ts'], + coverage: true, + coverageInclude: ['**/*.ts'], + coverageExclude: ['**/error.ts'], }); const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - const summary = harness.readFile('coverage/coverage-summary.json'); - expect(summary).not.toContain('"src/app/error.ts"'); + const summary = harness.readFile('coverage/test/coverage-final.json'); + expect(summary).not.toContain('src/app/error.ts"'); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-reporters_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-reporters_spec.ts index 4d0515d43cf3..edebb9e28167 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-reporters_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage-reporters_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "codeCoverageReporters"', () => { + describe('Option: "coverageReporters"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); @@ -23,26 +23,26 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { it('should generate a json summary report when specified', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - codeCoverage: true, - codeCoverageReporters: ['json-summary'] as any, + coverage: true, + coverageReporters: ['json-summary'] as any, }); const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(harness.hasFile('coverage/coverage-summary.json')).toBeTrue(); + expect(harness.hasFile('coverage/test/coverage-summary.json')).toBeTrue(); }); it('should generate multiple reports when specified', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - codeCoverage: true, - codeCoverageReporters: ['json-summary', 'lcov'] as any, + coverage: true, + coverageReporters: ['json-summary', 'lcov'] as any, }); const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(harness.hasFile('coverage/coverage-summary.json')).toBeTrue(); - expect(harness.hasFile('coverage/lcov.info')).toBeTrue(); + expect(harness.hasFile('coverage/test/coverage-summary.json')).toBeTrue(); + expect(harness.hasFile('coverage/test/lcov.info')).toBeTrue(); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts index 0264a58b58e9..f8a8acb60591 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/code-coverage_spec.ts @@ -15,31 +15,43 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "codeCoverage"', () => { + describe('Option: "coverage"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); - it('should not generate a code coverage report when codeCoverage is false', async () => { + it('should not generate a code coverage report when coverage is false', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - codeCoverage: false, + coverage: false, }); const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(harness.hasFile('coverage/index.html')).toBeFalse(); + harness.expectFile('coverage/test/index.html').toNotExist(); }); - it('should generate a code coverage report when codeCoverage is true', async () => { + it('should generate a code coverage report when coverage is true', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - codeCoverage: true, + coverage: true, }); const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(harness.hasFile('coverage/index.html')).toBeTrue(); + harness.expectFile('coverage/test/index.html').toExist(); + }); + + it('should generate a code coverage report when coverage is true', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + coverage: true, + coverageReporters: ['json'] as any, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('coverage/test/coverage-final.json').content.toContain('app.component.ts'); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/debug_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/debug_spec.ts index 965d08ce3079..9028ee0eb5fd 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/debug_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/debug_spec.ts @@ -12,6 +12,8 @@ import { describeBuilder, UNIT_TEST_BUILDER_INFO, setupApplicationTarget, + expectLog, + expectNoLog, } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { @@ -28,9 +30,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/Node.js inspector/) }), - ); + expectNoLog(logs, /Node.js inspector/); }); it('should enter debug mode when debug is true', async () => { @@ -41,11 +41,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(/Node.js inspector is active/), - }), - ); + expectLog(logs, /Node.js inspector is active/); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/exclude_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/exclude_spec.ts index 036adf8be63f..2c21f7680716 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/exclude_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/exclude_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "exclude"', () => { + describe('Option: "exclude"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); @@ -59,15 +59,5 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); }); - - it(`should exclude spec that matches the 'exclude' pattern prefixed with a slash`, async () => { - harness.useTarget('test', { - ...BASE_OPTIONS, - exclude: ['/src/app/error.spec.ts'], - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts new file mode 100644 index 000000000000..abcfe5976f94 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../builder'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "filter"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + + await harness.writeFiles({ + 'src/app/pass.spec.ts': ` + describe('Passing Suite', () => { + it('should pass', () => { + expect(true).toBe(true); + }); + }); + `, + 'src/app/fail.spec.ts': ` + describe('Failing Suite', () => { + it('should fail', () => { + expect(true).toBe(false); + }); + }); + `, + }); + }); + + it('should only run tests that match the filter regex', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + // This filter should only match the 'should pass' test + filter: 'pass$', + }); + + const { result } = await harness.executeOnce(); + // The overall result should be success because the failing test was filtered out. + expect(result?.success).toBe(true); + }); + + it('should run all tests when no filter is provided', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + // The overall result should be failure because the failing test was included. + expect(result?.success).toBe(false); + }); + }); +}); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts index be759449d8bc..f1cdd4a14f0b 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "include"', () => { + describe('Option: "include"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); @@ -35,18 +35,10 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { test: 'relative path from workspace to spec', input: ['src/app/app.component.spec.ts'], }, - { - test: 'relative path from workspace to file', - input: ['src/app/app.component.ts'], - }, { test: 'relative path from project root to spec', input: ['app/services/test.service.spec.ts'], }, - { - test: 'relative path from project root to file', - input: ['app/services/test.service.ts'], - }, { test: 'relative path from workspace to directory', input: ['src/app/services'], @@ -59,10 +51,6 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { test: 'glob with spec suffix', input: ['**/*.pipe.spec.ts', '**/*.pipe.spec.ts', '**/*test.service.spec.ts'], }, - { - test: 'glob with forward slash and spec suffix', - input: ['/**/*test.service.spec.ts'], - }, ].forEach((options, index) => { it(`should work with ${options.test} (${index})`, async () => { await harness.writeFiles({ diff --git a/packages/angular/build/src/builders/unit-test/tests/options/list-tests_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/list-tests_spec.ts new file mode 100644 index 000000000000..ade399d1936f --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/list-tests_spec.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../index'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "listTests"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + + await harness.writeFiles({ + 'src/app/app.component.spec.ts': + 'describe("AppComponent", () => { it("should...", () => {}); });', + 'src/app/other.spec.ts': 'describe("Other", () => { it("should...", () => {}); });', + 'src/app/ignored.ts': 'export const a = 1;', + }); + }); + + it('should list all discovered tests and exit when true', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + listTests: true, + }); + + const { result, logs } = await harness.executeOnce(); + + // Should succeed and exit without running tests + expect(result?.success).toBe(true); + + // Should log the discovered test files + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/Discovered test files:/) }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/.*app\.component\.spec\.ts/), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/.*other\.spec\.ts/) }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/.*ignore\.ts/) }), + ); + + // Should NOT log output from the test runner (since it shouldn't run) + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Application bundle generation complete/), + }), + ); + }); + + it('should not list tests and should run them as normal when false', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + listTests: false, + }); + + const { result, logs } = await harness.executeOnce(); + + // Should succeed because the tests pass + expect(result?.success).toBe(true); + + // Should NOT log the discovered test files + expect(logs).not.toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/Discovered test files:/) }), + ); + + // Should log output from the test runner + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Application bundle generation complete/), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts index d5b64923d70f..d69f6480c54d 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "providersFile"', () => { + describe('Option: "providersFile"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); @@ -26,10 +26,12 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { providersFile: 'src/my.providers.ts', }); - const { result, error } = await harness.executeOnce({ outputLogsOnFailure: false }); - expect(result).toBeUndefined(); - expect(error?.message).toMatch( - `The specified providers file "src/my.providers.ts" does not exist.`, + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Could not resolve "./my.providers"'), + }), ); }); @@ -42,6 +44,14 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { `, }); + await harness.modifyFile('src/tsconfig.spec.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('my.providers.ts'); + + return JSON.stringify(tsConfig); + }); + harness.useTarget('test', { ...BASE_OPTIONS, providersFile: 'src/my.providers.ts', diff --git a/packages/angular/build/src/builders/unit-test/tests/options/reporter_and_output_file_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/reporter_and_output_file_spec.ts new file mode 100644 index 000000000000..31fc1a7bfe95 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/reporter_and_output_file_spec.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../index'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Options: "reporter" and "outputFile"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + }); + + it(`should output a JSON report`, async () => { + await harness.removeFile('src/app/app.component.spec.ts'); + await harness.writeFiles({ + 'src/app/services/test.service.spec.ts': ` + describe('TestService', () => { + it('should succeed', () => { + expect(true).toBe(true); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: ['json'], + outputFile: 'test-report.json', + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + const reportContent = await harness.readFile('test-report.json'); + expect(reportContent).toContain('TestService'); + }); + }); +}); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts index 657d688fb6dc..89ac5804e37e 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts @@ -15,34 +15,66 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "reporters"', () => { - beforeEach(async () => { + describe('Option: "reporters"', () => { + beforeEach(() => { setupApplicationTarget(harness); }); - it('should use the default reporter when none is specified', async () => { + it(`should support a single reporter`, async () => { harness.useTarget('test', { ...BASE_OPTIONS, + reporters: ['json'], }); - const { result, logs } = await harness.executeOnce(); + const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/DefaultReporter/) }), - ); }); - it('should use a custom reporter when specified', async () => { + it(`should support multiple reporters`, async () => { harness.useTarget('test', { ...BASE_OPTIONS, - reporters: ['json'], + reporters: ['json', 'verbose'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it(`should support a single reporter with options`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: [['json', { outputFile: 'a.json' }]], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('a.json').toExist(); + }); + + it(`should support multiple reporters with options`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: [ + ['json', { outputFile: 'a.json' }], + ['junit', { outputFile: 'a.xml' }], + ], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('a.json').toExist(); + harness.expectFile('a.xml').toExist(); + }); + + it(`should support multiple reporters with and without options`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: [['json', { outputFile: 'a.json' }], 'verbose', 'default'], }); - const { result, logs } = await harness.executeOnce(); + const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/JsonReporter/) }), - ); + harness.expectFile('a.json').toExist(); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts new file mode 100644 index 000000000000..b061b88e990c --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../index'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +const VITEST_CONFIG_CONTENT = ` +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + test: { + reporters: [['junit', { outputFile: './vitest-results.xml' }]], + }, +}); +`; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "runnerConfig"', () => { + beforeEach(() => { + setupApplicationTarget(harness); + }); + + describe('Vitest Runner', () => { + it('should use a specified config file path', async () => { + harness.writeFile('custom-vitest.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'custom-vitest.config.ts', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should search for a config file when `true`', async () => { + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should ignore config file when `false`', async () => { + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: false, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toNotExist(); + }); + + it('should ignore config file by default', async () => { + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toNotExist(); + }); + + it('should find and use a `vitest-base.config.mts` in the project root', async () => { + harness.writeFile('vitest-base.config.mts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should find and use a `vitest-base.config.js` in the workspace root', async () => { + // This file should be ignored because the new logic looks for `vitest-base.config.*`. + harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + // The workspace root is the directory containing the project root in the test harness. + harness.writeFile('vitest-base.config.js', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should fallback to in-memory config when no base config is found', async () => { + // This file should be ignored because the new logic looks for `vitest-base.config.*` + // and when `runnerConfig` is true, it should not fall back to the default search. + harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toNotExist(); + }); + }); + }); +}); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/setup-files_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/setup-files_spec.ts index 8a461be4fd1f..5f888ed7ff64 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/setup-files_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/setup-files_spec.ts @@ -12,6 +12,7 @@ import { describeBuilder, UNIT_TEST_BUILDER_INFO, setupApplicationTarget, + expectLog, } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { @@ -50,7 +51,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain(jasmine.objectContaining({ message: 'Hello from setup.ts' })); + expectLog(logs, 'Hello from setup.ts'); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/tsconfig_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/tsconfig_spec.ts index 85723f71c4bf..101f07c1d12e 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/tsconfig_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/tsconfig_spec.ts @@ -12,43 +12,48 @@ import { describeBuilder, UNIT_TEST_BUILDER_INFO, setupApplicationTarget, + expectLog, } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "tsConfig"', () => { + describe('Option: "tsConfig"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); - it('should fail when tsConfig is not provided', async () => { + it('should use "tsconfig.spec.json" by default when it exists', async () => { const { tsConfig, ...rest } = BASE_OPTIONS; - harness.useTarget('test', rest as any); + harness.useTarget('test', rest); - await expectAsync(harness.executeOnce()).toBeRejectedWithError(/"tsConfig" is required/); + // Create tsconfig.spec.json + await harness.writeFile( + 'tsconfig.spec.json', + `{ "extends": "./tsconfig.json", "compilerOptions": { "types": ["jasmine"] }, "include": ["src/**/*.ts"] }`, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + // TODO: Add expectation that the file was used. }); - it('should fail when tsConfig is empty', async () => { - harness.useTarget('test', { - ...BASE_OPTIONS, - tsConfig: '', - }); + it('should use build target tsConfig when "tsconfig.spec.json" does not exist', async () => { + const { tsConfig, ...rest } = BASE_OPTIONS; + harness.useTarget('test', rest); - await expectAsync(harness.executeOnce()).toBeRejectedWithError( - /must NOT have fewer than 1 characters/, - ); + // The build target tsconfig is not setup to build the tests and should fail + const { result } = await harness.executeOnce(); + expect(result?.success).toBeFalse(); }); - it('should fail when tsConfig does not exist', async () => { + it('should fail when user specified tsConfig does not exist', async () => { harness.useTarget('test', { ...BASE_OPTIONS, - tsConfig: 'src/tsconfig.spec.json', + tsConfig: 'random/tsconfig.spec.json', }); - const { result, error } = await harness.executeOnce({ outputLogsOnFailure: false }); - expect(result).toBeUndefined(); - expect(error?.message).toMatch( - `The specified tsConfig file "src/tsconfig.spec.json" does not exist.`, - ); + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expectLog(logs, `The specified tsConfig file 'random/tsconfig.spec.json' does not exist.`); }); }); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts index 98434f302d57..d1840beb4a96 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/watch_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "watch"', () => { + describe('Option: "watch"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); diff --git a/packages/angular/build/src/builders/unit-test/tests/setup.ts b/packages/angular/build/src/builders/unit-test/tests/setup.ts index 80e0426ec3e4..db03c2b0b348 100644 --- a/packages/angular/build/src/builders/unit-test/tests/setup.ts +++ b/packages/angular/build/src/builders/unit-test/tests/setup.ts @@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { BuilderHarness } from '../../../../../../../modules/testing/builder/src'; import { - ApplicationBuilderOptions as AppilicationSchema, + ApplicationBuilderOptions as ApplicationSchema, buildApplication, } from '../../../builders/application'; import { Schema } from '../schema'; @@ -33,7 +33,7 @@ export const APPLICATION_BUILDER_INFO = Object.freeze({ * Contains all required application builder fields. * Also disables progress reporting to minimize logging output. */ -export const APPLICATION_BASE_OPTIONS = Object.freeze({ +export const APPLICATION_BASE_OPTIONS = Object.freeze({ index: 'src/index.html', browser: 'src/main.ts', outputPath: 'dist', @@ -84,7 +84,7 @@ let applicationSchema: json.schema.JsonSchema | undefined; */ export function setupApplicationTarget( harness: BuilderHarness, - extraOptions?: Partial, + extraOptions?: Partial, ): void { applicationSchema ??= JSON.parse( readFileSync(APPLICATION_BUILDER_INFO.schemaPath, 'utf8'), @@ -95,6 +95,7 @@ export function setupApplicationTarget( buildApplication, { ...APPLICATION_BASE_OPTIONS, + polyfills: ['zone.js', '@angular/localize/init'], ...extraOptions, }, { diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index e572ac02b216..5368eb3574e5 100644 --- a/packages/angular/build/src/private.ts +++ b/packages/angular/build/src/private.ts @@ -25,7 +25,7 @@ import { BundleStylesheetOptions } from './tools/esbuild/stylesheets/bundle-opti export { buildApplicationInternal } from './builders/application'; export type { ApplicationBuilderInternalOptions } from './builders/application/options'; export { type Result, type ResultFile, ResultKind } from './builders/application/results'; -export { serveWithVite } from './builders/dev-server/vite-server'; +export { serveWithVite } from './builders/dev-server/vite'; // Tools export * from './tools/babel/plugins'; diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts index bea3c65e1b8a..f22e8c813ecc 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts @@ -9,7 +9,6 @@ import type ng from '@angular/compiler-cli'; import type { PartialMessage } from 'esbuild'; import type ts from 'typescript'; -import { loadEsmModule } from '../../../utils/load-esm'; import { convertTypeScriptDiagnostic } from '../../esbuild/angular/diagnostics'; import { profileAsync, profileSync } from '../../esbuild/profiling'; import type { AngularHostOptions } from '../angular-host'; @@ -33,10 +32,7 @@ export abstract class AngularCompilation { static #typescriptModule?: typeof ts; static async loadCompilerCli(): Promise { - // This uses a wrapped dynamic import to load `@angular/compiler-cli` which is ESM. - // Once TypeScript provides support for retaining dynamic imports this workaround can be dropped. - AngularCompilation.#angularCompilerCliModule ??= - await loadEsmModule('@angular/compiler-cli'); + AngularCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli'); return AngularCompilation.#angularCompilerCliModule; } diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts index a811cb50ec0a..d2876654a15e 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -9,7 +9,6 @@ import type ng from '@angular/compiler-cli'; import assert from 'node:assert'; import ts from 'typescript'; -import { loadEsmModule } from '../../../utils/load-esm'; import { profileSync } from '../../esbuild/profiling'; import { AngularHostOptions, createAngularCompilerHost } from '../angular-host'; import { createJitResourceTransformer } from '../transformers/jit-resource-transformer'; @@ -44,9 +43,9 @@ export class JitCompilation extends AngularCompilation { referencedFiles: readonly string[]; }> { // Dynamically load the Angular compiler CLI package - const { constructorParametersDownlevelTransform } = await loadEsmModule< - typeof import('@angular/compiler-cli/private/tooling') - >('@angular/compiler-cli/private/tooling'); + const { constructorParametersDownlevelTransform } = await import( + '@angular/compiler-cli/private/tooling' + ); // Load the compiler configuration and transform as needed const { diff --git a/packages/angular/build/src/tools/angular/transformers/jit-bootstrap-transformer.ts b/packages/angular/build/src/tools/angular/transformers/jit-bootstrap-transformer.ts index 89b96244b765..d1d0a76df85d 100644 --- a/packages/angular/build/src/tools/angular/transformers/jit-bootstrap-transformer.ts +++ b/packages/angular/build/src/tools/angular/transformers/jit-bootstrap-transformer.ts @@ -32,7 +32,7 @@ export function replaceBootstrap( bootstrapImport = nodeFactory.createImportDeclaration( undefined, nodeFactory.createImportClause( - false, + undefined, undefined, nodeFactory.createNamespaceImport(bootstrapNamespace), ), diff --git a/packages/angular/build/src/tools/angular/transformers/jit-resource-transformer.ts b/packages/angular/build/src/tools/angular/transformers/jit-resource-transformer.ts index 3d7878378537..2a064ba63cb5 100644 --- a/packages/angular/build/src/tools/angular/transformers/jit-resource-transformer.ts +++ b/packages/angular/build/src/tools/angular/transformers/jit-resource-transformer.ts @@ -254,7 +254,7 @@ function createResourceImport( resourceImportDeclarations.push( nodeFactory.createImportDeclaration( undefined, - nodeFactory.createImportClause(false, importName, undefined), + nodeFactory.createImportClause(undefined, importName, undefined), urlLiteral, ), ); diff --git a/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions.ts b/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions.ts index ce47977a74e3..5aec104a38d2 100644 --- a/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions.ts +++ b/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions.ts @@ -6,12 +6,17 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { PluginObj } from '@babel/core'; +import type { NodePath, PluginObj, PluginPass, types } from '@babel/core'; import annotateAsPure from '@babel/helper-annotate-as-pure'; import * as tslib from 'tslib'; /** - * A cached set of TypeScript helper function names used by the helper name matcher utility function. + * A set of constructor names that are considered to be side-effect free. + */ +const sideEffectFreeConstructors = new Set(['InjectionToken']); + +/** + * A set of TypeScript helper function names used by the helper name matcher utility function. */ const tslibHelpers = new Set(Object.keys(tslib).filter((h) => h.startsWith('__'))); @@ -44,15 +49,23 @@ function isBabelHelperName(name: string): boolean { return babelHelpers.has(name); } +interface ExtendedPluginPass extends PluginPass { + opts: { topLevelSafeMode?: boolean }; +} + /** * A babel plugin factory function for adding the PURE annotation to top-level new and call expressions. - * * @returns A babel plugin object instance. */ export default function (): PluginObj { return { visitor: { - CallExpression(path) { + CallExpression(path: NodePath, state: ExtendedPluginPass) { + const { topLevelSafeMode = false } = state.opts; + if (topLevelSafeMode) { + return; + } + // If the expression has a function parent, it is not top-level if (path.getFunctionParent()) { return; @@ -65,6 +78,7 @@ export default function (): PluginObj { ) { return; } + // Do not annotate TypeScript helpers emitted by the TypeScript compiler or Babel helpers. // They are intended to cause side effects. if ( @@ -76,9 +90,22 @@ export default function (): PluginObj { annotateAsPure(path); }, - NewExpression(path) { + NewExpression(path: NodePath, state: ExtendedPluginPass) { // If the expression has a function parent, it is not top-level - if (!path.getFunctionParent()) { + if (path.getFunctionParent()) { + return; + } + + const { topLevelSafeMode = false } = state.opts; + + if (!topLevelSafeMode) { + annotateAsPure(path); + + return; + } + + const callee = path.get('callee'); + if (callee.isIdentifier() && sideEffectFreeConstructors.has(callee.node.name)) { annotateAsPure(path); } }, diff --git a/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_spec.ts b/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_spec.ts index 891f794f43d5..0966a67d068a 100644 --- a/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_spec.ts +++ b/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_spec.ts @@ -7,23 +7,24 @@ */ import { transformSync } from '@babel/core'; -// eslint-disable-next-line import/no-extraneous-dependencies import { format } from 'prettier'; import pureTopLevelPlugin from './pure-toplevel-functions'; function testCase({ input, expected, + options, }: { input: string; expected: string; + options?: { topLevelSafeMode: boolean }; }): jasmine.ImplementationCallback { return async () => { const result = transformSync(input, { configFile: false, babelrc: false, compact: true, - plugins: [pureTopLevelPlugin], + plugins: [[pureTopLevelPlugin, options]], }); if (!result?.code) { fail('Expected babel to return a transform result.'); @@ -152,4 +153,33 @@ describe('pure-toplevel-functions Babel plugin', () => { }; `), ); + + describe('topLevelSafeMode: true', () => { + it( + 'annotates top-level `new InjectionToken` expressions', + testCase({ + input: `const result = new InjectionToken('abc');`, + expected: `const result = /*#__PURE__*/ new InjectionToken('abc');`, + options: { topLevelSafeMode: true }, + }), + ); + + it( + 'does not annotate other top-level `new` expressions', + testCase({ + input: 'const result = new SomeClass();', + expected: 'const result = new SomeClass();', + options: { topLevelSafeMode: true }, + }), + ); + + it( + 'does not annotate top-level function calls', + testCase({ + input: 'const result = someCall();', + expected: 'const result = someCall();', + options: { topLevelSafeMode: true }, + }), + ); + }); }); diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index f56e6c6c3119..eb9daa3b6155 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -13,11 +13,13 @@ import type { OnStartResult, OutputFile, PartialMessage, + PartialNote, Plugin, PluginBuild, } from 'esbuild'; import assert from 'node:assert'; import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; import { maxWorkers, useTypeChecking } from '../../../utils/environment-options'; import { AngularHostOptions } from '../../angular/angular-host'; @@ -29,6 +31,7 @@ import { SharedTSCompilationState, getSharedCompilationState } from './compilati import { ComponentStylesheetBundler } from './component-stylesheets'; import { FileReferenceTracker } from './file-reference-tracker'; import { setupJitPluginCallbacks } from './jit-plugin-callbacks'; +import { rewriteForBazel } from './rewrite-bazel-paths'; import { SourceFileCache } from './source-file-cache'; export interface CompilerPluginOptions { @@ -411,8 +414,8 @@ export function createCompilerPlugin( }); build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => { - const request = path.normalize( - pluginOptions.fileReplacements?.[path.normalize(args.path)] ?? args.path, + const request = rewriteForBazel( + path.normalize(pluginOptions.fileReplacements?.[path.normalize(args.path)] ?? args.path), ); const isJS = /\.[cm]?js$/.test(request); @@ -440,11 +443,23 @@ export function createCompilerPlugin( return undefined; } + const diangosticRoot = build.initialOptions.absWorkingDir ?? ''; + + // Evaluate whether the file requires the Angular compiler transpilation. + // If not, issue a warning but allow bundler to process the file (no type-checking). + const directContents = await readFile(request, 'utf-8'); + if (!requiresAngularCompiler(directContents)) { + return { + warnings: [createMissingFileDiagnostic(request, args.path, diangosticRoot, false)], + contents, + loader: 'ts', + resolveDir: path.dirname(request), + }; + } + // Otherwise return an error return { - errors: [ - createMissingFileError(request, args.path, build.initialOptions.absWorkingDir ?? ''), - ], + errors: [createMissingFileDiagnostic(request, args.path, diangosticRoot, true)], }; } else if (typeof contents === 'string' && (useTypeScriptTranspilation || isJS)) { // A string indicates untransformed output from the TS/NG compiler. @@ -478,13 +493,14 @@ export function createCompilerPlugin( return { contents, loader, + resolveDir: path.dirname(request), }; }); build.onLoad( { filter: /\.[cm]?js$/ }, createCachedLoad(pluginOptions.loadResultCache, async (args) => { - let request = args.path; + let request = rewriteForBazel(args.path); if (pluginOptions.fileReplacements) { const replacement = pluginOptions.fileReplacements[path.normalize(args.path)]; if (replacement) { @@ -505,6 +521,7 @@ export function createCompilerPlugin( return { contents, loader: 'js', + resolveDir: path.dirname(request), watchFiles: request !== args.path ? [request] : undefined, }; }, @@ -759,23 +776,46 @@ function bundleWebWorker( } } -function createMissingFileError(request: string, original: string, root: string): PartialMessage { +function createMissingFileDiagnostic( + request: string, + original: string, + root: string, + angular: boolean, +): PartialMessage { const relativeRequest = path.relative(root, request); - const error = { - text: `File '${relativeRequest}' is missing from the TypeScript compilation.`, - notes: [ - { - text: `Ensure the file is part of the TypeScript program via the 'files' or 'include' property.`, - }, - ], - }; + const notes: PartialNote[] = []; + + if (angular) { + notes.push({ + text: + `Files containing Angular metadata ('@Component'/'@Directive'/etc.) must be part of the TypeScript compilation.` + + ` You can ensure the file is part of the TypeScript program via the 'files' or 'include' property.`, + }); + } else { + notes.push({ + text: + `The file will be bundled and included in the output but will not be type-checked at build time.` + + ` To remove this message you can add the file to the TypeScript program via the 'files' or 'include' property.`, + }); + } const relativeOriginal = path.relative(root, original); if (relativeRequest !== relativeOriginal) { - error.notes.push({ + notes.push({ text: `File is requested from a file replacement of '${relativeOriginal}'.`, }); } - return error; + const diagnostic = { + text: `File '${relativeRequest}' not found in TypeScript compilation.`, + notes, + }; + + return diagnostic; +} + +const POTENTIAL_METADATA_REGEX = /@angular\/core|@Component|@Directive|@Injectable|@Pipe|@NgModule/; + +function requiresAngularCompiler(contents: string): boolean { + return POTENTIAL_METADATA_REGEX.test(contents); } diff --git a/packages/angular/build/src/tools/esbuild/angular/rewrite-bazel-paths.ts b/packages/angular/build/src/tools/esbuild/angular/rewrite-bazel-paths.ts new file mode 100644 index 000000000000..8a6fb6aa82a4 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/angular/rewrite-bazel-paths.ts @@ -0,0 +1,30 @@ +/** + * @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 { join, relative } from 'node:path'; + +const bazelBinDirectory = process.env['BAZEL_BINDIR']; +const bazelExecRoot = process.env['JS_BINARY__EXECROOT']; + +export function rewriteForBazel(path: string): string { + if (!bazelBinDirectory || !bazelExecRoot) { + return path; + } + + const fromExecRoot = relative(bazelExecRoot, path); + if (!fromExecRoot.startsWith('..')) { + return path; + } + + const fromBinDirectory = relative(bazelBinDirectory, path); + if (fromBinDirectory.startsWith('..')) { + return path; + } + + return join(bazelExecRoot, fromBinDirectory); +} diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index b17029f6c5e1..635faca8c82e 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -654,7 +654,7 @@ function getEsBuildCommonPolyfillsOptions( tryToResolvePolyfillsAsRelative: boolean, loadResultCache: LoadResultCache | undefined, ): BuildOptions | undefined { - const { jit, workspaceRoot, i18nOptions } = options; + const { jit, workspaceRoot, i18nOptions, externalPackages } = options; const buildOptions = getEsBuildCommonOptions(options); buildOptions.splitting = false; @@ -671,8 +671,10 @@ function getEsBuildCommonPolyfillsOptions( // Locale data should go first so that project provided polyfill code can augment if needed. let needLocaleDataPlugin = false; if (i18nOptions.shouldInline) { - // Remove localize polyfill as this is not needed for build time i18n. - polyfills = polyfills.filter((path) => !path.startsWith('@angular/localize')); + if (!externalPackages) { + // Remove localize polyfill when i18n inline transformation have been applied to all the packages. + polyfills = polyfills.filter((path) => !path.startsWith('@angular/localize')); + } // Add locale data for all active locales // TODO: Inject each individually within the inlining process itself @@ -686,7 +688,7 @@ function getEsBuildCommonPolyfillsOptions( needLocaleDataPlugin = true; } if (needLocaleDataPlugin) { - buildOptions.plugins.push(createAngularLocaleDataPlugin()); + buildOptions.plugins.unshift(createAngularLocaleDataPlugin()); } if (polyfills.length === 0) { diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts index 9caa2ca5da45..74550e83e5de 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts @@ -11,7 +11,6 @@ import { PluginObj, parseSync, transformFromAstAsync, types } from '@babel/core' import assert from 'node:assert'; import { workerData } from 'node:worker_threads'; import { assertIsError } from '../../utils/error'; -import { loadEsmModule } from '../../utils/load-esm'; /** * The options passed to the inliner for each file request @@ -131,7 +130,7 @@ async function loadLocalizeTools(): Promise { // Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround. // Once TypeScript provides support for keeping the dynamic import this workaround can be // changed to a direct dynamic import. - localizeToolsModule ??= await loadEsmModule('@angular/localize/tools'); + localizeToolsModule ??= await import('@angular/localize/tools'); return localizeToolsModule; } diff --git a/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts b/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts index 30f4540dc3a8..ae94b62ca16d 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { Plugin } from 'esbuild'; +import type { Plugin, ResolveResult } from 'esbuild'; +import { createRequire } from 'node:module'; /** * The internal namespace used by generated locale import statements and Angular locale data plugin. @@ -27,17 +28,6 @@ export function createAngularLocaleDataPlugin(): Plugin { return { name: 'angular-locale-data', setup(build): void { - // If packages are configured to be external then leave the original angular locale import path. - // This happens when using the development server with caching enabled to allow Vite prebundling to work. - // There currently is no option on the esbuild resolve function to resolve while disabling the option. To - // workaround the inability to resolve the full locale location here, the Vite dev server prebundling also - // contains a plugin to allow the locales to be correctly resolved when prebundling. - // NOTE: If esbuild eventually allows controlling the external package options in a build.resolve call, this - // workaround can be removed. - if (build.initialOptions.packages === 'external') { - return; - } - build.onResolve({ filter: /^angular:locale\/data:/ }, async ({ path }) => { // Extract the locale from the path const rawLocaleTag = path.split(':', 3)[2]; @@ -60,6 +50,7 @@ export function createAngularLocaleDataPlugin(): Plugin { } let exact = true; + let localeRequire: NodeJS.Require | undefined; while (partialLocaleTag) { // Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here. // The onLoad hook below for the locale data namespace has an `empty` loader that will prevent inclusion. @@ -73,11 +64,39 @@ export function createAngularLocaleDataPlugin(): Plugin { // Attempt to resolve the locale tag data within the Angular base module location const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocaleTag}`; - const result = await build.resolve(potentialPath, { - kind: 'import-statement', - resolveDir: build.initialOptions.absWorkingDir, - }); - if (result.path) { + + // If packages are configured to be external then leave the original angular locale import path. + // This happens when using the development server with caching enabled to allow Vite prebundling to work. + // There currently is no option on the esbuild resolve function to resolve while disabling the option. + // NOTE: If esbuild eventually allows controlling the external package options in a build.resolve call, this + // workaround can be removed. + let result: ResolveResult | undefined; + const { packages, absWorkingDir } = build.initialOptions; + if (packages === 'external' && absWorkingDir) { + localeRequire ??= createRequire(absWorkingDir + '/'); + + try { + localeRequire.resolve(potentialPath); + + result = { + errors: [], + warnings: [], + external: true, + sideEffects: true, + namespace: '', + suffix: '', + pluginData: undefined, + path: potentialPath, + }; + } catch {} + } else { + result = await build.resolve(potentialPath, { + kind: 'import-statement', + resolveDir: absWorkingDir, + }); + } + + if (result?.path) { if (exact) { return result; } else { diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts index 8fa551b38ba2..7d86009b773f 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts @@ -10,7 +10,6 @@ import { type PluginItem, transformAsync } from '@babel/core'; import fs from 'node:fs'; import path from 'node:path'; import Piscina from 'piscina'; -import { loadEsmModule } from '../../utils/load-esm'; interface JavaScriptTransformRequest { filename: string; @@ -78,21 +77,19 @@ async function transformWithBabel( } if (options.advancedOptimizations) { - const sideEffectFree = options.sideEffects === false; - const safeAngularPackage = - sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); - const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } = await import('../babel/plugins'); - if (safeAngularPackage) { - plugins.push(markTopLevelPure); - } + const sideEffectFree = options.sideEffects === false; + const safeAngularPackage = + sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); - plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [ - adjustStaticMembers, - { wrapDecorators: sideEffectFree }, - ]); + plugins.push( + [markTopLevelPure, { topLevelSafeMode: !safeAngularPackage }], + elideAngularMetadata, + adjustTypeScriptEnums, + [adjustStaticMembers, { wrapDecorators: sideEffectFree }], + ); } // If no additional transformations are needed, return the data directly @@ -135,11 +132,8 @@ async function requiresLinking(path: string, source: string): Promise { } async function createLinkerPlugin(options: Omit) { - linkerPluginCreator ??= ( - await loadEsmModule( - '@angular/compiler-cli/linker/babel', - ) - ).createEs2015LinkerPlugin; + linkerPluginCreator ??= (await import('@angular/compiler-cli/linker/babel')) + .createEs2015LinkerPlugin; const linkerPlugin = linkerPluginCreator({ linkerJitMode: options.jit, diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts index b27b8a087b46..b728a0f599e2 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts @@ -125,7 +125,7 @@ export class JavaScriptTransformer { { // The below is disable as with Yarn PNP this causes build failures with the below message // `Unable to deserialize cloned data`. - transferList: process.versions.pnp ? undefined : [data.buffer as ArrayBuffer], + transferList: process.versions.pnp ? undefined : [data.buffer], }, )) as Uint8Array; 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 9bbcb7c5ecb8..7fa20dde64ae 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts @@ -31,7 +31,7 @@ export interface BundleStylesheetOptions { externalDependencies?: string[]; target: string[]; tailwindConfiguration?: { file: string; package: string }; - postcssConfiguration?: PostcssConfiguration; + postcssConfiguration?: { config: PostcssConfiguration; configPath: string }; publicPath?: string; cacheOptions: NormalizedCachedOptions; } diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/less-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/less-language.ts index 1f6d8f2b7c26..68cacd576b10 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/less-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/less-language.ts @@ -32,13 +32,7 @@ export const LessStylesheetLanguage = Object.freeze({ componentFilter: /^less;/, fileFilter: /\.less$/, process(data, file, _, options, build) { - return compileString( - data, - file, - options, - build.resolve.bind(build), - /* unsafeInlineJavaScript */ false, - ); + return compileString(data, file, options, build.resolve.bind(build)); }, }); @@ -47,7 +41,6 @@ async function compileString( filename: string, options: StylesheetPluginOptions, resolver: PluginBuild['resolve'], - unsafeInlineJavaScript: boolean, ): Promise { try { lessPreprocessor ??= (await import('less')).default; @@ -118,7 +111,6 @@ async function compileString( paths: options.includePaths, plugins: [resolverPlugin], rewriteUrls: 'all', - javascriptEnabled: unsafeInlineJavaScript, sourceMap: options.sourcemap ? { sourceMapFileInline: true, @@ -136,35 +128,6 @@ async function compileString( if (isLessException(error)) { const location = convertExceptionLocation(error); - // Retry with a warning for less files requiring the deprecated inline JavaScript option - if (error.message.includes('Inline JavaScript is not enabled.')) { - const withJsResult = await compileString( - data, - filename, - options, - resolver, - /* unsafeInlineJavaScript */ true, - ); - withJsResult.warnings = [ - { - text: 'Deprecated inline execution of JavaScript has been enabled ("javascriptEnabled")', - location, - notes: [ - { - location: null, - text: 'JavaScript found within less stylesheets may be executed at build time. [https://lesscss.org/usage/#less-options]', - }, - { - location: null, - text: 'Support for "javascriptEnabled" may be removed from the Angular CLI starting with Angular v19.', - }, - ], - }, - ]; - - return withJsResult; - } - return { errors: [ { diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts index f618bbf6cc39..78925f35835e 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts @@ -9,7 +9,8 @@ import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild'; import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { extname } from 'node:path'; +import { createRequire } from 'node:module'; +import { dirname, extname } from 'node:path'; import type { Options } from 'sass'; import { glob } from 'tinyglobby'; import { assertIsError } from '../../../utils/error'; @@ -69,7 +70,7 @@ export interface StylesheetPluginOptions { * initialized and used for every stylesheet. This overrides the tailwind integration * and any tailwind usage must be manually configured in the custom postcss usage. */ - postcssConfiguration?: PostcssConfiguration; + postcssConfiguration?: { config: PostcssConfiguration; configPath: string }; /** * Optional Options for configuring Sass behavior. @@ -215,14 +216,18 @@ export class StylesheetPluginFactory { const { options } = this; if (options.postcssConfiguration) { - const postCssInstanceKey = JSON.stringify(options.postcssConfiguration); + const { config, configPath } = options.postcssConfiguration; + const postCssInstanceKey = JSON.stringify(config); let postcssProcessor = postcssProcessors.get(postCssInstanceKey)?.deref(); if (!postcssProcessor) { postcss ??= (await import('postcss')).default; postcssProcessor = postcss(); - for (const [pluginName, pluginOptions] of options.postcssConfiguration.plugins) { - const { default: plugin } = await import(pluginName); + + const postCssPluginRequire = createRequire(dirname(configPath) + '/'); + for (const [pluginName, pluginOptions] of config.plugins) { + const pluginMod = postCssPluginRequire(pluginName); + const plugin = pluginMod.__esModule ? pluginMod['default'] : pluginMod; if (typeof plugin !== 'function' || plugin.postcss !== true) { throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`); } diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts index a9fe69ffca15..414ec5ca13d1 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -12,6 +12,7 @@ import { readFileSync } from 'node:fs'; import type { ServerResponse } from 'node:http'; import { extname } from 'node:path'; import type { Connect, ViteDevServer } from 'vite'; +import { ResultFile } from '../../../builders/application/results'; import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils'; export interface ComponentStyleRecord { @@ -214,3 +215,45 @@ function checkAndHandleEtag( return false; } + +export function createBuildAssetsMiddleware( + basePath: string, + buildResultFiles: ReadonlyMap, + readHandler: (path: string) => Buffer = readFileSync, +): Connect.NextHandleFunction { + return function buildAssetsMiddleware(req, res, next) { + if (req.url === undefined || res.writableEnded) { + return; + } + + // Parse the incoming request. + // The base of the URL is unused but required to parse the URL. + const pathname = pathnameWithoutBasePath(req.url, basePath); + const extension = extname(pathname); + if (extension && !/\.[mc]?[jt]s(?:\.map)?$/.test(extension)) { + const outputFile = buildResultFiles.get(pathname.slice(1)); + if (outputFile) { + const contents = + outputFile.origin === 'memory' ? outputFile.contents : readHandler(outputFile.inputPath); + + const etag = `W/${createHash('sha256').update(contents).digest('hex')}`; + if (checkAndHandleEtag(req, res, etag)) { + return; + } + + const mimeType = lookupMimeType(extension); + if (mimeType) { + res.setHeader('Content-Type', mimeType); + } + + res.setHeader('ETag', etag); + res.setHeader('Cache-Control', 'no-cache'); + res.end(contents); + + return; + } + } + + next(); + }; +} diff --git a/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts index 387a94a2ba53..0d8ac5441f1d 100644 --- a/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts @@ -12,7 +12,6 @@ import type { } from '@angular/ssr'; import type { ServerResponse } from 'node:http'; import type { Connect, ViteDevServer } from 'vite'; -import { loadEsmModule } from '../../../utils/load-esm'; import { isSsrNodeRequestHandler, isSsrRequestHandler, @@ -36,9 +35,11 @@ export function createAngularSsrInternalMiddleware( (async () => { // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, // which must be processed by the runtime linker, even if they are not used. - await loadEsmModule('@angular/compiler'); - const { writeResponseToNodeResponse, createWebRequestFromNodeRequest } = - await loadEsmModule('@angular/ssr/node'); + await import('@angular/compiler'); + const { writeResponseToNodeResponse, createWebRequestFromNodeRequest } = (await import( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + '@angular/ssr/node' as any + )) as typeof import('@angular/ssr/node', { with: { 'resolution-mode': 'import' } }); const { ɵgetOrCreateAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as { ɵgetOrCreateAngularServerApp: typeof getOrCreateAngularServerApp; @@ -90,10 +91,12 @@ export async function createAngularSsrExternalMiddleware( // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, // which must be processed by the runtime linker, even if they are not used. - await loadEsmModule('@angular/compiler'); + await import('@angular/compiler'); - const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = - await loadEsmModule('@angular/ssr/node'); + const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = (await import( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + '@angular/ssr/node' as any + )) as typeof import('@angular/ssr/node', { with: { 'resolution-mode': 'import' } }); return function angularSsrExternalMiddleware( req: Connect.IncomingMessage, diff --git a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts index 5fc178b60386..be00e3437f27 100644 --- a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts @@ -11,7 +11,6 @@ import { readFile } from 'node:fs/promises'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Plugin } from 'vite'; -import { loadEsmModule } from '../../../utils/load-esm'; import { AngularMemoryOutputFiles } from '../utils'; interface AngularMemoryPluginOptions { @@ -30,7 +29,7 @@ export async function createAngularMemoryPlugin( options: AngularMemoryPluginOptions, ): Promise { const { virtualProjectRoot, outputFiles, external } = options; - const { normalizePath } = await loadEsmModule('vite'); + const { normalizePath } = await import('vite'); return { name: 'vite:angular-memory', diff --git a/packages/angular/build/src/tools/vite/plugins/i18n-locale-plugin.ts b/packages/angular/build/src/tools/vite/plugins/i18n-locale-plugin.ts deleted file mode 100644 index 5cf3762245a5..000000000000 --- a/packages/angular/build/src/tools/vite/plugins/i18n-locale-plugin.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import type { Plugin } from 'vite'; - -/** - * The base module location used to search for locale specific data. - */ -const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global'; - -/** - * Creates a Vite plugin that resolves Angular locale data files from `@angular/common`. - * - * @returns A Vite plugin. - */ -export function createAngularLocaleDataPlugin(): Plugin { - return { - name: 'angular-locale-data', - enforce: 'pre', - async resolveId(source) { - if (!source.startsWith('angular:locale/data:')) { - return; - } - - // Extract the locale from the path - const originalLocale = source.split(':', 3)[2]; - - // Remove any private subtags since these will never match - let partialLocale = originalLocale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, ''); - - let exact = true; - while (partialLocale) { - const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocale}`; - - const result = await this.resolve(potentialPath); - if (result) { - if (!exact) { - this.warn( - `Locale data for '${originalLocale}' cannot be found. Using locale data for '${partialLocale}'.`, - ); - } - - return result; - } - - // Remove the last subtag and try again with a less specific locale - const parts = partialLocale.split('-'); - partialLocale = parts.slice(0, -1).join('-'); - exact = false; - // The locales "en" and "en-US" are considered exact to retain existing behavior - if (originalLocale === 'en-US' && partialLocale === 'en') { - exact = true; - } - } - - return null; - }, - }; -} diff --git a/packages/angular/build/src/tools/vite/plugins/index.ts b/packages/angular/build/src/tools/vite/plugins/index.ts index 50a6ab6aa7c9..6c4cdd4496e4 100644 --- a/packages/angular/build/src/tools/vite/plugins/index.ts +++ b/packages/angular/build/src/tools/vite/plugins/index.ts @@ -7,7 +7,7 @@ */ export { createAngularMemoryPlugin } from './angular-memory-plugin'; -export { createAngularLocaleDataPlugin } from './i18n-locale-plugin'; export { createRemoveIdPrefixPlugin } from './id-prefix-plugin'; export { createAngularSetupMiddlewaresPlugin, ServerSsrMode } from './setup-middlewares-plugin'; export { createAngularSsrTransformPlugin } from './ssr-transform-plugin'; +export { createAngularServerSideSSLPlugin } from './ssr-ssl-plugin'; diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index 21ddaa350ac1..b82cc2d3acd6 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -7,7 +7,6 @@ */ import type { Connect, Plugin } from 'vite'; -import { loadEsmModule } from '../../../utils/load-esm'; import { ComponentStyleRecord, angularHtmlFallbackMiddleware, @@ -61,8 +60,7 @@ interface AngularSetupMiddlewaresPluginOptions { async function createEncapsulateStyle(): Promise< (style: Uint8Array, componentId: string) => string > { - const { encapsulateStyle } = - await loadEsmModule('@angular/compiler'); + const { encapsulateStyle } = await import('@angular/compiler'); const decoder = new TextDecoder('utf-8'); return (style, componentId) => { diff --git a/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts new file mode 100644 index 000000000000..c87e0bd112a0 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts @@ -0,0 +1,46 @@ +/** + * @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 { rootCertificates } from 'node:tls'; +import type { Plugin } from 'vite'; + +export function createAngularServerSideSSLPlugin(): Plugin { + return { + name: 'angular-ssr-ssl-plugin', + apply: 'serve', + async configureServer({ config, httpServer }) { + const { + ssr, + server: { https }, + } = config; + + if (!ssr || !https?.cert) { + return; + } + + // TODO(alanagius): Replace `undici` with `tls.setDefaultCACertificates` once we only support Node.js 22.18.0+ and 24.5.0+. + // See: https://nodejs.org/api/tls.html#tlssetdefaultcacertificatescerts + const { getGlobalDispatcher, setGlobalDispatcher, Agent } = await import('undici'); + const originalDispatcher = getGlobalDispatcher(); + const { cert } = https; + const certificates = Array.isArray(cert) ? cert : [cert]; + + setGlobalDispatcher( + new Agent({ + connect: { + ca: [...rootCertificates, ...certificates], + }, + }), + ); + + httpServer?.on('close', () => { + setGlobalDispatcher(originalDispatcher); + }); + }, + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts b/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts index 0ac7b92d442d..90d183acde02 100644 --- a/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts @@ -8,10 +8,9 @@ import remapping, { SourceMapInput } from '@ampproject/remapping'; import type { Plugin } from 'vite'; -import { loadEsmModule } from '../../../utils/load-esm'; export async function createAngularSsrTransformPlugin(workspaceRoot: string): Promise { - const { normalizePath } = await loadEsmModule('vite'); + const { normalizePath } = await import('vite'); return { name: 'vite:angular-ssr-transform', diff --git a/packages/angular/build/src/typings.d.ts b/packages/angular/build/src/typings.d.ts deleted file mode 100644 index 6296581de448..000000000000 --- a/packages/angular/build/src/typings.d.ts +++ /dev/null @@ -1,26 +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 - */ - -// The `bundled_beasties` causes issues with module mappings in Bazel, -// leading to unexpected behavior with esbuild. Specifically, the problem occurs -// when esbuild resolves to a different module or version than expected, due to -// how Bazel handles module mappings. -// -// This change aims to resolve esbuild types correctly and maintain consistency -// in the Bazel build process. - -declare module 'esbuild' { - export * from 'esbuild-wasm'; -} - -/** - * Augment the Node.js module builtin types to support the v22.8+ compile cache functions - */ -declare module 'node:module' { - function getCompileCacheDir(): string | undefined; -} diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index ea06fea2d09f..80f71d56c119 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -8,22 +8,48 @@ import { availableParallelism } from 'node:os'; -function isDisabled(variable: string): boolean { - return variable === '0' || variable.toLowerCase() === 'false'; -} +/** A set of strings that are considered "truthy" when parsing environment variables. */ +const TRUTHY_VALUES = new Set(['1', 'true']); -function isEnabled(variable: string): boolean { - return variable === '1' || variable.toLowerCase() === 'true'; -} +/** A set of strings that are considered "falsy" when parsing environment variables. */ +const FALSY_VALUES = new Set(['0', 'false']); +/** + * Checks if an environment variable is present and has a non-empty value. + * @param variable The environment variable to check. + * @returns `true` if the variable is a non-empty string. + */ function isPresent(variable: string | undefined): variable is string { return typeof variable === 'string' && variable !== ''; } +/** + * Parses an environment variable into a boolean or undefined. + * @returns `true` if the variable is truthy ('1', 'true'). + * @returns `false` if the variable is falsy ('0', 'false'). + * @returns `undefined` if the variable is not present or has an unknown value. + */ +function parseTristate(variable: string | undefined): boolean | undefined { + if (!isPresent(variable)) { + return undefined; + } + + const value = variable.toLowerCase(); + if (TRUTHY_VALUES.has(value)) { + return true; + } + if (FALSY_VALUES.has(value)) { + return false; + } + + // TODO: Consider whether a warning is useful in this case of a malformed value + return undefined; +} + // Optimization and mangling const debugOptimizeVariable = process.env['NG_BUILD_DEBUG_OPTIMIZE']; const debugOptimize = (() => { - if (!isPresent(debugOptimizeVariable) || isDisabled(debugOptimizeVariable)) { + if (!isPresent(debugOptimizeVariable) || parseTristate(debugOptimizeVariable) === false) { return { mangle: true, minify: true, @@ -37,7 +63,7 @@ const debugOptimize = (() => { beautify: true, }; - if (isEnabled(debugOptimizeVariable)) { + if (parseTristate(debugOptimizeVariable) === true) { return debugValue; } @@ -58,12 +84,22 @@ const debugOptimize = (() => { return debugValue; })(); -const mangleVariable = process.env['NG_BUILD_MANGLE']; -export const allowMangle = isPresent(mangleVariable) - ? !isDisabled(mangleVariable) - : debugOptimize.mangle; +/** + * Allows disabling of code mangling when the `NG_BUILD_MANGLE` environment variable is set to `0` or `false`. + * This is useful for debugging build output. + */ +export const allowMangle = parseTristate(process.env['NG_BUILD_MANGLE']) ?? debugOptimize.mangle; +/** + * Allows beautification of build output when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. + * This is useful for debugging build output. + */ export const shouldBeautify = debugOptimize.beautify; + +/** + * Allows disabling of code minification when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. + * This is useful for debugging build output. + */ export const allowMinify = debugOptimize.minify; /** @@ -76,39 +112,64 @@ export const allowMinify = debugOptimize.minify; * */ const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS']; + +/** + * The maximum number of workers to use for parallel processing. + * This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable. + */ export const maxWorkers = isPresent(maxWorkersVariable) ? +maxWorkersVariable : Math.min(4, Math.max(availableParallelism() - 1, 1)); -const parallelTsVariable = process.env['NG_BUILD_PARALLEL_TS']; -export const useParallelTs = !isPresent(parallelTsVariable) || !isDisabled(parallelTsVariable); +/** + * When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled. + */ +export const useParallelTs = parseTristate(process.env['NG_BUILD_PARALLEL_TS']) !== false; -const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF']; -export const debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable); +/** + * When `NG_BUILD_DEBUG_PERF` is enabled, performance debugging information is printed. + */ +export const debugPerformance = parseTristate(process.env['NG_BUILD_DEBUG_PERF']) === true; -const watchRootVariable = process.env['NG_BUILD_WATCH_ROOT']; -export const shouldWatchRoot = isPresent(watchRootVariable) && isEnabled(watchRootVariable); +/** + * When `NG_BUILD_WATCH_ROOT` is enabled, the build will watch the root directory for changes. + */ +export const shouldWatchRoot = parseTristate(process.env['NG_BUILD_WATCH_ROOT']) === true; -const typeCheckingVariable = process.env['NG_BUILD_TYPE_CHECK']; -export const useTypeChecking = - !isPresent(typeCheckingVariable) || !isDisabled(typeCheckingVariable); +/** + * When `NG_BUILD_TYPE_CHECK` is set to `0` or `false`, type checking is disabled. + */ +export const useTypeChecking = parseTristate(process.env['NG_BUILD_TYPE_CHECK']) !== false; -const buildLogsJsonVariable = process.env['NG_BUILD_LOGS_JSON']; -export const useJSONBuildLogs = - isPresent(buildLogsJsonVariable) && isEnabled(buildLogsJsonVariable); +/** + * When `NG_BUILD_LOGS_JSON` is enabled, build logs will be output in JSON format. + */ +export const useJSONBuildLogs = parseTristate(process.env['NG_BUILD_LOGS_JSON']) === true; -const optimizeChunksVariable = process.env['NG_BUILD_OPTIMIZE_CHUNKS']; -export const shouldOptimizeChunks = - isPresent(optimizeChunksVariable) && isEnabled(optimizeChunksVariable); +/** + * When `NG_BUILD_OPTIMIZE_CHUNKS` is enabled, the build will optimize chunks. + */ +export const shouldOptimizeChunks = parseTristate(process.env['NG_BUILD_OPTIMIZE_CHUNKS']) === true; + +/** + * When `NG_HMR_CSTYLES` is enabled, component styles will be hot-reloaded. + */ +export const useComponentStyleHmr = parseTristate(process.env['NG_HMR_CSTYLES']) === true; -const hmrComponentStylesVariable = process.env['NG_HMR_CSTYLES']; -export const useComponentStyleHmr = - isPresent(hmrComponentStylesVariable) && isEnabled(hmrComponentStylesVariable); +/** + * When `NG_HMR_TEMPLATES` is set to `0` or `false`, component templates will not be hot-reloaded. + */ +export const useComponentTemplateHmr = parseTristate(process.env['NG_HMR_TEMPLATES']) !== false; + +/** + * When `NG_BUILD_PARTIAL_SSR` is enabled, a partial server-side rendering build will be performed. + */ +export const usePartialSsrBuild = parseTristate(process.env['NG_BUILD_PARTIAL_SSR']) === true; -const hmrComponentTemplateVariable = process.env['NG_HMR_TEMPLATES']; -export const useComponentTemplateHmr = - !isPresent(hmrComponentTemplateVariable) || !isDisabled(hmrComponentTemplateVariable); +const bazelBinDirectory = process.env['BAZEL_BINDIR']; +const bazelExecRoot = process.env['JS_BINARY__EXECROOT']; -const partialSsrBuildVariable = process.env['NG_BUILD_PARTIAL_SSR']; -export const usePartialSsrBuild = - isPresent(partialSsrBuildVariable) && isEnabled(partialSsrBuildVariable); +export const bazelEsbuildPluginPath = + bazelBinDirectory && bazelExecRoot + ? process.env['NG_INTERNAL_ESBUILD_PLUGINS_DO_NOT_USE'] + : undefined; diff --git a/packages/angular/build/src/utils/index-file/augment-index-html.ts b/packages/angular/build/src/utils/index-file/augment-index-html.ts index 7a30770fa450..21ba39b39cc4 100644 --- a/packages/angular/build/src/utils/index-file/augment-index-html.ts +++ b/packages/angular/build/src/utils/index-file/augment-index-html.ts @@ -8,7 +8,6 @@ import { createHash } from 'node:crypto'; import { extname } from 'node:path'; -import { loadEsmModule } from '../load-esm'; import { htmlRewritingStream } from './html-rewriting-stream'; import { VALID_SELF_CLOSING_TAGS } from './valid-self-closing-tags'; @@ -352,12 +351,7 @@ async function getLanguageDirection( async function getLanguageDirectionFromLocales(locale: string): Promise { try { - const localeData = ( - await loadEsmModule( - `@angular/common/locales/${locale}`, - ) - ).default; - + const localeData = (await import(`@angular/common/locales/${locale}`)).default; const dir = localeData[localeData.length - 2]; return isString(dir) ? dir : undefined; diff --git a/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts b/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts index 7ea16ab6121b..55adf8d88f0b 100644 --- a/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts +++ b/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { tags } from '@angular-devkit/core'; import { AugmentIndexHtmlOptions, augmentIndexHtml } from './augment-index-html'; describe('augment-index-html', () => { @@ -25,7 +24,7 @@ describe('augment-index-html', () => { }; const oneLineHtml = (html: TemplateStringsArray) => - tags.stripIndents`${html}`.replace(/(>\s+)/g, '>'); + `${html}`.replace(/(>\s+)/g, '>').replace(/\s+ { const { content } = await augmentIndexHtml({ diff --git a/packages/angular/build/src/utils/index-file/html-rewriting-stream.ts b/packages/angular/build/src/utils/index-file/html-rewriting-stream.ts index dbeeadcb2fc1..baf494be033c 100644 --- a/packages/angular/build/src/utils/index-file/html-rewriting-stream.ts +++ b/packages/angular/build/src/utils/index-file/html-rewriting-stream.ts @@ -9,7 +9,6 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import type { RewritingStream } from 'parse5-html-rewriting-stream'; -import { loadEsmModule } from '../load-esm'; // Export helper types for the rewriter export type StartTag = Parameters[0]; @@ -20,9 +19,7 @@ export async function htmlRewritingStream(content: string): Promise<{ rewriter: RewritingStream; transformedContent: () => Promise; }> { - const { RewritingStream } = await loadEsmModule( - 'parse5-html-rewriting-stream', - ); + const { RewritingStream } = await import('parse5-html-rewriting-stream'); const rewriter = new RewritingStream(); return { diff --git a/packages/angular/build/src/utils/load-proxy-config.ts b/packages/angular/build/src/utils/load-proxy-config.ts index e590ee9efc6c..cf4cb9e3c03e 100644 --- a/packages/angular/build/src/utils/load-proxy-config.ts +++ b/packages/angular/build/src/utils/load-proxy-config.ts @@ -49,33 +49,20 @@ export async function loadProxyConfiguration( break; } - case '.mjs': - // Load the ESM configuration file using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - proxyConfiguration = await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath)); - break; - case '.cjs': - proxyConfiguration = require(proxyPath); - break; - default: - // The file could be either CommonJS or ESM. - // CommonJS is tried first then ESM if loading fails. + default: { try { - proxyConfiguration = require(proxyPath); - break; + proxyConfiguration = await import(proxyPath); } catch (e) { assertIsError(e); - if (e.code === 'ERR_REQUIRE_ESM' || e.code === 'ERR_REQUIRE_ASYNC_MODULE') { - // Load the ESM configuration file using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - proxyConfiguration = await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath)); - break; + if (e.code !== 'ERR_REQUIRE_ASYNC_MODULE') { + throw e; } - throw e; + proxyConfiguration = await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath)); } + + break; + } } if ('default' in proxyConfiguration) { diff --git a/packages/angular/build/src/utils/load-translations.ts b/packages/angular/build/src/utils/load-translations.ts index eeac280cbf9b..e202273dc73d 100644 --- a/packages/angular/build/src/utils/load-translations.ts +++ b/packages/angular/build/src/utils/load-translations.ts @@ -9,7 +9,6 @@ import type { Diagnostics } from '@angular/localize/tools'; import { createHash } from 'node:crypto'; import * as fs from 'node:fs'; -import { loadEsmModule } from './load-esm'; export type TranslationLoader = (path: string) => { translations: Record; @@ -62,7 +61,7 @@ async function importParsers() { Xliff1TranslationParser, Xliff2TranslationParser, XtbTranslationParser, - } = await loadEsmModule('@angular/localize/tools'); + } = await import('@angular/localize/tools'); const diagnostics = new Diagnostics(); const parsers = { diff --git a/packages/angular/build/src/utils/postcss-configuration.ts b/packages/angular/build/src/utils/postcss-configuration.ts index 1861f9f2b1db..6f3f1f3671f9 100644 --- a/packages/angular/build/src/utils/postcss-configuration.ts +++ b/packages/angular/build/src/utils/postcss-configuration.ts @@ -71,9 +71,13 @@ async function readPostcssConfiguration( return config; } -export async function loadPostcssConfiguration( - searchDirectories: SearchDirectory[], -): Promise { +export async function loadPostcssConfiguration(searchDirectories: SearchDirectory[]): Promise< + | { + configPath: string; + config: PostcssConfiguration; + } + | undefined +> { const configPath = findFile(searchDirectories, postcssConfigurationFiles); if (!configPath) { return undefined; @@ -101,7 +105,7 @@ export async function loadPostcssConfiguration( } } - return config; + return { config, configPath }; } // Normalize plugin object map form @@ -119,5 +123,5 @@ export async function loadPostcssConfiguration( config.plugins.push([name, options]); } - return config; + return { config, configPath }; } diff --git a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts index 9e1a657366a0..1d0d9df32d30 100644 --- a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts +++ b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts @@ -8,9 +8,9 @@ import assert from 'node:assert'; import { randomUUID } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer'; /** * @note For some unknown reason, setting `globalThis.ngServerMode = true` does not work when using ESM loader hooks. @@ -32,14 +32,6 @@ export interface ESMInMemoryFileLoaderWorkerData { let memoryVirtualRootUrl: string; let outputFiles: Record; -const javascriptTransformer = new JavaScriptTransformer( - // Always enable JIT linking to support applications built with and without AOT. - // In a development environment the additional scope information does not - // have a negative effect unlike production where final output size is relevant. - { sourcemap: true, jit: true }, - 1, -); - export function initialize(data: ESMInMemoryFileLoaderWorkerData) { // This path does not actually exist but is used to overlay the in memory files with the // actual filesystem for resolution purposes. @@ -137,7 +129,7 @@ export async function load(url: string, context: { format?: string | null }, nex // need linking are ESM only. if (format === 'module' && isFileProtocol(url)) { const filePath = fileURLToPath(url); - let source = await javascriptTransformer.transformFile(filePath); + let source = await readFile(filePath); if (filePath.includes('@angular/')) { // Prepend 'var ngServerMode=true;' to the source. @@ -158,11 +150,3 @@ export async function load(url: string, context: { format?: string | null }, nex function isFileProtocol(url: string): boolean { return url.startsWith('file://'); } - -function handleProcessExit(): void { - void javascriptTransformer.close(); -} - -process.once('exit', handleProcessExit); -process.once('SIGINT', handleProcessExit); -process.once('uncaughtException', handleProcessExit); diff --git a/packages/angular/build/src/utils/server-rendering/launch-server.ts b/packages/angular/build/src/utils/server-rendering/launch-server.ts index cfb15b0d979b..c17337178b13 100644 --- a/packages/angular/build/src/utils/server-rendering/launch-server.ts +++ b/packages/angular/build/src/utils/server-rendering/launch-server.ts @@ -8,7 +8,6 @@ import assert from 'node:assert'; import { createServer } from 'node:http'; -import { loadEsmModule } from '../load-esm'; import { loadEsmModuleFromMemory } from './load-esm-from-memory'; import { isSsrNodeRequestHandler, isSsrRequestHandler } from './utils'; @@ -21,8 +20,10 @@ export const DEFAULT_URL = new URL('http://ng-localhost/'); */ export async function launchServer(): Promise { const { reqHandler } = await loadEsmModuleFromMemory('./server.mjs'); - const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = - await loadEsmModule('@angular/ssr/node'); + const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = (await import( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + '@angular/ssr/node' as any + )) as typeof import('@angular/ssr/node', { with: { 'resolution-mode': 'import' } }); if (!isSsrNodeRequestHandler(reqHandler) && !isSsrRequestHandler(reqHandler)) { return DEFAULT_URL; diff --git a/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts b/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts index 1d19a07e61de..87ca9928a86f 100644 --- a/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts +++ b/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts @@ -7,6 +7,7 @@ */ import type { ApplicationRef, Type } from '@angular/core'; +import type { BootstrapContext } from '@angular/platform-browser'; import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr'; import { assertIsError } from '../error'; import { loadEsmModule } from '../load-esm'; @@ -15,7 +16,7 @@ import { loadEsmModule } from '../load-esm'; * Represents the exports available from the main server bundle. */ interface MainServerBundleExports { - default: (() => Promise) | Type; + default: ((context: BootstrapContext) => Promise) | Type; ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree; ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp; } diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index e087262a7f0c..5ece379ec9c0 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -26,6 +26,7 @@ import { WritableSerializableRouteTreeNode, } from './models'; import type { RenderWorkerData } from './render-worker'; +import { generateRedirectStaticPage } from './utils'; type PrerenderOptions = NormalizedApplicationBuildOptions['prerenderOptions']; type AppShellOptions = NormalizedApplicationBuildOptions['appShellOptions']; @@ -380,28 +381,3 @@ function addTrailingSlash(url: string): string { function removeLeadingSlash(value: string): string { return value[0] === '/' ? value.slice(1) : value; } - -/** - * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL. - * - * This function creates a simple HTML page that performs a redirect using a meta tag. - * It includes a fallback link in case the meta-refresh doesn't work. - * - * @param url - The URL to which the page should redirect. - * @returns The HTML content of the static redirect page. - */ -function generateRedirectStaticPage(url: string): string { - return ` - - - - - Redirecting - - - -
Redirecting to ${url}
- - -`.trim(); -} diff --git a/packages/angular/build/src/utils/server-rendering/render-worker.ts b/packages/angular/build/src/utils/server-rendering/render-worker.ts index 92fad35df32a..7ded0550b826 100644 --- a/packages/angular/build/src/utils/server-rendering/render-worker.ts +++ b/packages/angular/build/src/utils/server-rendering/render-worker.ts @@ -12,6 +12,7 @@ import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loa import { patchFetchToLoadInMemoryAssets } from './fetch-patch'; import { DEFAULT_URL, launchServer } from './launch-server'; import { loadEsmModuleFromMemory } from './load-esm-from-memory'; +import { generateRedirectStaticPage } from './utils'; export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData { assetFiles: Record; @@ -48,10 +49,20 @@ async function renderPage({ url }: RenderOptions): Promise { new Request(new URL(url, serverURL), { signal: AbortSignal.timeout(30_000) }), ); - return response ? response.text() : null; + if (!response) { + return null; + } + + const location = response.headers.get('Location'); + + return location ? generateRedirectStaticPage(location) : response.text(); } async function initialize() { + // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, + // which must be processed by the runtime linker, even if they are not used. + await import('@angular/compiler'); + if (outputMode !== undefined && hasSsrEntry) { serverURL = await launchServer(); } diff --git a/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts b/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts index 1487db07e811..423a71e83ba5 100644 --- a/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts +++ b/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts @@ -28,6 +28,10 @@ const { outputMode, hasSsrEntry } = workerData as { /** Renders an application based on a provided options. */ async function extractRoutes(): Promise { + // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, + // which must be processed by the runtime linker, even if they are not used. + await import('@angular/compiler'); + const serverURL = outputMode !== undefined && hasSsrEntry ? await launchServer() : DEFAULT_URL; patchFetchToLoadInMemoryAssets(serverURL); diff --git a/packages/angular/build/src/utils/server-rendering/utils.ts b/packages/angular/build/src/utils/server-rendering/utils.ts index 83c90187178b..c740d4de06c4 100644 --- a/packages/angular/build/src/utils/server-rendering/utils.ts +++ b/packages/angular/build/src/utils/server-rendering/utils.ts @@ -7,7 +7,7 @@ */ import type { createRequestHandler } from '@angular/ssr'; -import type { createNodeRequestHandler } from '@angular/ssr/node'; +import type { createNodeRequestHandler } from '@angular/ssr/node' with { 'resolution-mode': 'import' }; export function isSsrNodeRequestHandler( value: unknown, @@ -19,3 +19,28 @@ export function isSsrRequestHandler( ): value is ReturnType { return typeof value === 'function' && '__ng_request_handler__' in value; } + +/** + * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL. + * + * This function creates a simple HTML page that performs a redirect using a meta tag. + * It includes a fallback link in case the meta-refresh doesn't work. + * + * @param url - The URL to which the page should redirect. + * @returns The HTML content of the static redirect page. + */ +export function generateRedirectStaticPage(url: string): string { + return ` + + + + + Redirecting + + + +
Redirecting to ${url}
+ + +`.trim(); +} diff --git a/packages/angular/build/src/utils/service-worker.ts b/packages/angular/build/src/utils/service-worker.ts index c6f95f99a595..1535684f635c 100644 --- a/packages/angular/build/src/utils/service-worker.ts +++ b/packages/angular/build/src/utils/service-worker.ts @@ -6,14 +6,16 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { Config, Filesystem } from '@angular/service-worker/config'; +import type { + Config, + Filesystem, +} from '@angular/service-worker/config' with { 'resolution-mode': 'import' }; import * as crypto from 'node:crypto'; -import { existsSync, constants as fsConstants, promises as fsPromises } from 'node:fs'; +import { existsSync, promises as fsPromises } from 'node:fs'; import * as path from 'node:path'; import { BuildOutputFile, BuildOutputFileType } from '../tools/esbuild/bundler-context'; import { BuildOutputAsset } from '../tools/esbuild/bundler-execution-result'; import { assertIsError } from './error'; -import { loadEsmModule } from './load-esm'; import { toPosixPath } from './path'; class CliFilesystem implements Filesystem { @@ -216,17 +218,14 @@ export async function augmentAppWithServiceWorkerCore( serviceWorkerFilesystem: Filesystem, baseHref: string, ): Promise<{ manifest: string; assetFiles: { source: string; destination: string }[] }> { - // Load ESM `@angular/service-worker/config` using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - const GeneratorConstructor = ( - await loadEsmModule( - '@angular/service-worker/config', - ) - ).Generator; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { Generator } = (await import('@angular/service-worker/config' as any)) as typeof import( + '@angular/service-worker/config', + { with: { 'resolution-mode': 'import' } } + ); // Generate the manifest - const generator = new GeneratorConstructor(serviceWorkerFilesystem, baseHref); + const generator = new Generator(serviceWorkerFilesystem, baseHref); const output = await generator.process(config); // Write the manifest diff --git a/packages/angular/build/src/utils/supported-browsers.ts b/packages/angular/build/src/utils/supported-browsers.ts index a8546a039082..d871d75789d3 100644 --- a/packages/angular/build/src/utils/supported-browsers.ts +++ b/packages/angular/build/src/utils/supported-browsers.ts @@ -8,14 +8,15 @@ import browserslist from 'browserslist'; +// The below is replaced by bazel `npm_package`. +const BASELINE_DATE = 'BASELINE-DATE-PLACEHOLDER'; + export function getSupportedBrowsers( projectRoot: string, logger: { warn(message: string): void }, ): string[] { // Read the browserslist configuration containing Angular's browser support policy. - const angularBrowserslist = browserslist(undefined, { - path: require.resolve('../../.browserslistrc'), - }); + const angularBrowserslist = browserslist(`baseline widely available on ${getBaselineDate()}`); // Use Angular's configuration as the default. browserslist.defaults = angularBrowserslist; @@ -60,3 +61,8 @@ export function getSupportedBrowsers( return Array.from(browsersFromConfigOrDefault); } + +function getBaselineDate(): string { + // Unlike `npm_package`, `ts_project` which is used to run unit tests does not support substitutions. + return BASELINE_DATE[0] === 'B' ? '2025-01-01' : BASELINE_DATE; +} diff --git a/packages/angular/build/src/utils/test-files.ts b/packages/angular/build/src/utils/test-files.ts new file mode 100644 index 000000000000..522bb1e778c0 --- /dev/null +++ b/packages/angular/build/src/utils/test-files.ts @@ -0,0 +1,51 @@ +/** + * @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 * as fs from 'node:fs/promises'; +import path from 'node:path'; +import { ResultFile } from '../builders/application/results'; +import { BuildOutputFileType } from '../tools/esbuild/bundler-context'; +import { emitFilesToDisk } from '../tools/esbuild/utils'; + +/** + * Writes a collection of build result files to a specified directory. + * This function handles both in-memory and on-disk files, creating subdirectories + * as needed. + * + * @param files A map of file paths to `ResultFile` objects, representing the build output. + * @param testDir The absolute path to the directory where the files should be written. + */ +export async function writeTestFiles( + files: Record, + testDir: string, +): Promise { + const directoryExists = new Set(); + // Writes the test related output files to disk and ensures the containing directories are present + await emitFilesToDisk(Object.entries(files), async ([filePath, file]) => { + if (file.type !== BuildOutputFileType.Browser && file.type !== BuildOutputFileType.Media) { + return; + } + + const fullFilePath = path.join(testDir, filePath); + + // Ensure output subdirectories exist + const fileBasePath = path.dirname(fullFilePath); + if (fileBasePath && !directoryExists.has(fileBasePath)) { + await fs.mkdir(fileBasePath, { recursive: true }); + directoryExists.add(fileBasePath); + } + + if (file.origin === 'memory') { + // Write file contents + await fs.writeFile(fullFilePath, file.contents); + } else { + // Copy file contents + await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE); + } + }); +} diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 409dbcd14000..e173d31e5413 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -38,6 +38,7 @@ ts_project( ], exclude = [ "**/*_spec.ts", + "**/testing/**", ], ) + [ # These files are generated from the JSON schema @@ -58,6 +59,7 @@ ts_project( ":node_modules/jsonc-parser", ":node_modules/npm-package-arg", ":node_modules/pacote", + ":node_modules/parse5-html-rewriting-stream", ":node_modules/resolve", ":node_modules/yargs", ":node_modules/zod", @@ -72,6 +74,7 @@ ts_project( "//:node_modules/@types/yarnpkg__lockfile", "//:node_modules/listr2", "//:node_modules/semver", + "//:node_modules/typescript", ], ) @@ -114,7 +117,10 @@ ts_project( name = "angular-cli_test_lib", testonly = True, srcs = glob( - include = ["**/*_spec.ts"], + include = [ + "**/*_spec.ts", + "**/testing/**", + ], exclude = [ # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces "node_modules/**", @@ -124,10 +130,12 @@ ts_project( ":angular-cli", ":node_modules/@angular-devkit/core", ":node_modules/@angular-devkit/schematics", + ":node_modules/@modelcontextprotocol/sdk", ":node_modules/yargs", "//:node_modules/@types/semver", "//:node_modules/@types/yargs", "//:node_modules/semver", + "//:node_modules/typescript", ], ) diff --git a/packages/angular/cli/lib/config/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json index 0c551dc4fb14..3fede1746559 100644 --- a/packages/angular/cli/lib/config/workspace-schema.json +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -47,7 +47,7 @@ "packageManager": { "description": "Specify which package manager tool to use.", "type": "string", - "enum": ["npm", "cnpm", "yarn", "pnpm", "bun"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "warnings": { "description": "Control CLI specific console warnings", @@ -101,7 +101,7 @@ "packageManager": { "description": "Specify which package manager tool to use.", "type": "string", - "enum": ["npm", "cnpm", "yarn", "pnpm", "bun"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "warnings": { "description": "Control CLI specific console warnings", diff --git a/packages/angular/cli/lib/examples/if-block.md b/packages/angular/cli/lib/examples/if-block.md index e0d10ca86891..806e3d05516c 100644 --- a/packages/angular/cli/lib/examples/if-block.md +++ b/packages/angular/cli/lib/examples/if-block.md @@ -1,28 +1,85 @@ -# Angular @if Control Flow Example +--- +title: 'Using the @if Built-in Control Flow Block' +summary: 'Demonstrates how to use the @if built-in control flow block to conditionally render content in an Angular template based on a boolean expression.' +keywords: + - '@if' + - 'control flow' + - 'conditional rendering' + - 'template syntax' +related_concepts: + - '@else' + - '@else if' + - 'signals' +related_tools: + - 'modernize' +--- -This example demonstrates how to use the `@if` control flow block in an Angular template. The visibility of a `
` element is controlled by a boolean field in the component's TypeScript code. +## Purpose -## Angular Template +The purpose of this pattern is to create dynamic user interfaces by controlling which elements are rendered to the DOM based on the application's state. This is a fundamental technique for building responsive and interactive components. -```html - -@if (isVisible()) { -
This content is conditionally displayed.
+## When to Use + +Use the `@if` block as the modern, preferred alternative to the `*ngIf` directive for all conditional rendering. It offers better type-checking and a cleaner, more intuitive syntax within the template. + +## Key Concepts + +- **`@if` block:** The primary syntax for conditional rendering in modern Angular templates. It evaluates a boolean expression and renders the content within its block if the expression is true. + +## Example Files + +### `conditional-content.component.ts` + +This is a self-contained standalone component that demonstrates the `@if` block with an optional `@else` block. + +```typescript +import { Component, signal } from '@angular/core'; + +@Component({ + selector: 'app-conditional-content', + template: ` + + + @if (isVisible()) { +
This content is conditionally displayed.
+ } @else { +
The content is hidden. Click the button to show it.
+ } + `, +}) +export class ConditionalContentComponent { + protected readonly isVisible = signal(true); + + toggleVisibility(): void { + this.isVisible.update((v) => !v); + } } ``` -## Component TypeScript +## Usage Notes + +- The expression inside the `@if ()` block must evaluate to a boolean. +- This example uses a signal, which is a common pattern, but any boolean property or method call from the component can be used. +- The `@else` block is optional and is rendered when the `@if` condition is `false`. + +## How to Use This Example + +### 1. Import the Component + +In a standalone architecture, import the component into the `imports` array of the parent component where you want to use it. ```typescript +// in app.component.ts import { Component } from '@angular/core'; +import { ConditionalContentComponent } from './conditional-content.component'; @Component({ - selector: 'app-example', - templateUrl: './example.html', - styleUrl: './example.css', + selector: 'app-root', + imports: [ConditionalContentComponent], + template: ` +

My Application

+ + `, }) -export class Example { - // This boolean signal controls the visibility of the element in the template. - protected readonly isVisible = signal(true); -} +export class AppComponent {} ``` diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 29234e9a3e15..bfa371a8d796 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -25,19 +25,20 @@ "@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER", "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", "@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER", - "@inquirer/prompts": "7.8.1", - "@listr2/prompt-adapter-inquirer": "3.0.1", - "@modelcontextprotocol/sdk": "1.17.2", + "@inquirer/prompts": "7.9.0", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.21.0", "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.35.0", - "ini": "5.0.0", + "algoliasearch": "5.42.0", + "ini": "6.0.0", "jsonc-parser": "3.3.1", - "listr2": "9.0.1", - "npm-package-arg": "13.0.0", - "pacote": "21.0.0", - "resolve": "1.22.10", - "semver": "7.7.2", + "listr2": "9.0.5", + "npm-package-arg": "13.0.1", + "pacote": "21.0.3", + "parse5-html-rewriting-stream": "8.0.0", + "resolve": "1.22.11", + "semver": "7.7.3", "yargs": "18.0.0", "zod": "3.25.76" }, diff --git a/packages/angular/cli/src/command-builder/architect-command-module.ts b/packages/angular/cli/src/command-builder/architect-command-module.ts index 4855b629b360..98e270cf1dad 100644 --- a/packages/angular/cli/src/command-builder/architect-command-module.ts +++ b/packages/angular/cli/src/command-builder/architect-command-module.ts @@ -41,7 +41,8 @@ export abstract class ArchitectCommandModule // Add default builder if target is not in project and a command default is provided if (this.findDefaultBuilderName && this.context.workspace) { for (const [project, projectDefinition] of this.context.workspace.projects) { - if (projectDefinition.targets.has(target)) { + const targetDefinition = projectDefinition.targets.get(target); + if (targetDefinition?.builder) { continue; } @@ -49,7 +50,13 @@ export abstract class ArchitectCommandModule project, target, }); - if (defaultBuilder) { + if (!defaultBuilder) { + continue; + } + + if (targetDefinition) { + targetDefinition.builder = defaultBuilder; + } else { projectDefinition.targets.set(target, { builder: defaultBuilder, }); @@ -97,11 +104,17 @@ export abstract class ArchitectCommandModule } async run(options: Options & OtherOptions): Promise { - const target = this.getArchitectTarget(); + const originalProcessTitle = process.title; + try { + const target = this.getArchitectTarget(); + const { configuration = '', project, ...architectOptions } = options; - const { configuration = '', project, ...architectOptions } = options; + if (project) { + process.title = `${originalProcessTitle} (${project})`; + + return await this.runSingleTarget({ configuration, target, project }, architectOptions); + } - if (!project) { // This runs each target sequentially. // Running them in parallel would jumble the log messages. let result = 0; @@ -111,12 +124,13 @@ export abstract class ArchitectCommandModule } for (const project of projectNames) { + process.title = `${originalProcessTitle} (${project})`; result |= await this.runSingleTarget({ configuration, target, project }, architectOptions); } return result; - } else { - return await this.runSingleTarget({ configuration, target, project }, architectOptions); + } finally { + process.title = originalProcessTitle; } } diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 0b18512180d2..64f7ac7377c7 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -89,7 +89,10 @@ export abstract class CommandModule implements CommandModuleI protected readonly shouldReportAnalytics: boolean = true; readonly scope: CommandScope = CommandScope.Both; - private readonly optionsWithAnalytics = new Map(); + private readonly optionsWithAnalytics = new Map< + string, + EventCustomDimension | EventCustomMetric + >(); constructor(protected readonly context: CommandContext) {} @@ -236,12 +239,16 @@ export abstract class CommandModule implements CommandModuleI ]); for (const [name, ua] of this.optionsWithAnalytics) { + if (!validEventCustomDimensionAndMetrics.has(ua)) { + continue; + } + const value = options[name]; - if ( - (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') && - validEventCustomDimensionAndMetrics.has(ua as EventCustomDimension | EventCustomMetric) - ) { - parameters[ua as EventCustomDimension | EventCustomMetric] = value; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + parameters[ua] = value; + } else if (Array.isArray(value)) { + // GA doesn't allow array as values. + parameters[ua] = value.sort().join(', '); } } diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts index 8843ea09d461..cb4ab2c8467e 100644 --- a/packages/angular/cli/src/command-builder/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -8,7 +8,7 @@ import { logging } from '@angular-devkit/core'; import yargs from 'yargs'; -import { Parser } from 'yargs/helpers'; +import { Parser as yargsParser } from 'yargs/helpers'; import { CommandConfig, CommandNames, @@ -29,8 +29,6 @@ import { import { jsonHelpUsage } from './utilities/json-help'; import { createNormalizeOptionsMiddleware } from './utilities/normalize-options-middleware'; -const yargsParser = Parser as unknown as typeof Parser.default; - export async function runCommand(args: string[], logger: logging.Logger): Promise { const { $0, diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts index 738fd497382b..ef317700d1a6 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -204,11 +204,20 @@ export abstract class SchematicsCommandModule ? { name: item, value: item, + checked: + definition.multiselect && Array.isArray(definition.default) + ? definition.default?.includes(item) + : item === definition.default, } : { ...item, name: item.label, value: item.value, + checked: + definition.multiselect && Array.isArray(definition.default) + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + definition.default?.includes(item.value as any) + : item.value === definition.default, }, ), }); diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts index 84af5f2d3641..e9fbd7e0e27a 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -import { json, strings } from '@angular-devkit/core'; +import { isJsonObject, json, strings } from '@angular-devkit/core'; import type { Arguments, Argv, PositionalOptions, Options as YargsOptions } from 'yargs'; +import { EventCustomDimension } from '../../analytics/analytics-parameters'; /** - * An option description. + * An option description that can be used by yargs to create a command. + * See: https://github.com/yargs/yargs/blob/main/docs/options.mjs */ export interface Option extends YargsOptions { /** @@ -50,10 +52,44 @@ export interface Option extends YargsOptions { itemValueType?: 'string'; } +/** + * A Yargs check function that validates that the given options are in the form of `key=value`. + * @param keyValuePairOptions A set of options that should be in the form of `key=value`. + * @param args The parsed arguments. + * @returns `true` if the options are valid, otherwise an error is thrown. + */ +function checkStringMap(keyValuePairOptions: Set, args: Arguments): boolean { + for (const key of keyValuePairOptions) { + const value = args[key]; + if (!Array.isArray(value)) { + // Value has been parsed. + continue; + } + + for (const pair of value) { + if (pair === undefined) { + continue; + } + + if (!pair.includes('=')) { + throw new Error( + `Invalid value for argument: ${key}, Given: '${pair}', Expected key=value pair`, + ); + } + } + } + + return true; +} + +/** + * A Yargs coerce function that converts an array of `key=value` strings to an object. + * @param value An array of `key=value` strings. + * @returns An object with the keys and values from the input array. + */ function coerceToStringMap( - dashedName: string, value: (string | undefined)[], -): Record | Promise { +): Record | (string | undefined)[] { const stringMap: Record = {}; for (const pair of value) { // This happens when the flag isn't passed at all. @@ -63,23 +99,23 @@ function coerceToStringMap( const eqIdx = pair.indexOf('='); if (eqIdx === -1) { - // TODO: Remove workaround once yargs properly handles thrown errors from coerce. - // Right now these sometimes end up as uncaught exceptions instead of proper validation - // errors with usage output. - return Promise.reject( - new Error( - `Invalid value for argument: ${dashedName}, Given: '${pair}', Expected key=value pair`, - ), - ); + // In the case it is not valid skip processing this option and handle the error in `checkStringMap` + return value; } + const key = pair.slice(0, eqIdx); - const value = pair.slice(eqIdx + 1); - stringMap[key] = value; + stringMap[key] = pair.slice(eqIdx + 1); } return stringMap; } +/** + * Checks if a JSON schema node represents a string map. + * A string map is an object with `additionalProperties` of type `string`. + * @param node The JSON schema node to check. + * @returns `true` if the node represents a string map, otherwise `false`. + */ function isStringMap(node: json.JsonObject): boolean { // Exclude fields with more specific kinds of properties. if (node.properties || node.patternProperties) { @@ -94,6 +130,179 @@ function isStringMap(node: json.JsonObject): boolean { ); } +const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string']); + +/** + * Checks if a string is a supported primitive type. + * @param value The string to check. + * @returns `true` if the string is a supported primitive type, otherwise `false`. + */ +function isSupportedPrimitiveType(value: string): boolean { + return SUPPORTED_PRIMITIVE_TYPES.has(value); +} + +/** + * Recursively checks if a JSON schema for an array's items is a supported primitive type. + * It supports `oneOf` and `anyOf` keywords. + * @param schema The JSON schema for the array's items. + * @returns `true` if the schema is a supported primitive type, otherwise `false`. + */ +function isSupportedArrayItemSchema(schema: json.JsonObject): boolean { + if (typeof schema.type === 'string' && isSupportedPrimitiveType(schema.type)) { + return true; + } + + if (json.isJsonArray(schema.enum)) { + return true; + } + + if (json.isJsonArray(schema.items)) { + return schema.items.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)); + } + + if ( + json.isJsonArray(schema.oneOf) && + schema.oneOf.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)) + ) { + return true; + } + + if ( + json.isJsonArray(schema.anyOf) && + schema.anyOf.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)) + ) { + return true; + } + + return false; +} + +/** + * Gets the supported types for a JSON schema node. + * @param current The JSON schema node to get the supported types for. + * @returns An array of supported types. + */ +function getSupportedTypes( + current: json.JsonObject, +): ReadonlyArray<'string' | 'number' | 'boolean' | 'array' | 'object'> { + const typeSet = json.schema.getTypesOfSchema(current); + + if (typeSet.size === 0) { + return []; + } + + return [...typeSet].filter((type) => { + switch (type) { + case 'boolean': + case 'number': + case 'string': + return true; + case 'array': + return isJsonObject(current.items) && isSupportedArrayItemSchema(current.items); + case 'object': + return isStringMap(current); + default: + return false; + } + }) as ReadonlyArray<'string' | 'number' | 'boolean' | 'array' | 'object'>; +} + +/** + * Gets the enum values for a JSON schema node. + * @param current The JSON schema node to get the enum values for. + * @returns An array of enum values. + */ +function getEnumValues( + current: json.JsonObject, +): ReadonlyArray | undefined { + if (json.isJsonArray(current.enum)) { + return current.enum.sort() as ReadonlyArray; + } + + if (isJsonObject(current.items)) { + const enumValues = getEnumValues(current.items); + if (enumValues?.length) { + return enumValues; + } + } + + if (typeof current.type === 'string' && isSupportedPrimitiveType(current.type)) { + return []; + } + + const subSchemas = + (json.isJsonArray(current.oneOf) && current.oneOf) || + (json.isJsonArray(current.anyOf) && current.anyOf); + + if (subSchemas) { + // Find the first enum. + for (const subSchema of subSchemas) { + if (isJsonObject(subSchema)) { + const enumValues = getEnumValues(subSchema); + if (enumValues) { + return enumValues; + } + } + } + } + + return []; +} + +/** + * Gets the default value for a JSON schema node. + * @param current The JSON schema node to get the default value for. + * @param type The type of the JSON schema node. + * @returns The default value, or `undefined` if there is no default value. + */ +function getDefaultValue( + current: json.JsonObject, + type: string, +): string | number | boolean | unknown[] | undefined { + const defaultValue = current.default; + if (defaultValue === undefined) { + return undefined; + } + + if (type === 'array') { + return Array.isArray(defaultValue) && defaultValue.length > 0 ? defaultValue : undefined; + } + + if (typeof defaultValue === type) { + return defaultValue as string | number | boolean; + } + + return undefined; +} + +/** + * Gets the aliases for a JSON schema node. + * @param current The JSON schema node to get the aliases for. + * @returns An array of aliases. + */ +function getAliases(current: json.JsonObject): string[] { + if (json.isJsonArray(current.aliases)) { + return [...current.aliases].map(String); + } + + if (current.alias) { + return [String(current.alias)]; + } + + return []; +} + +/** + * Parses a JSON schema to a list of options that can be used by yargs. + * + * @param registry A schema registry to use for flattening the schema. + * @param schema The JSON schema to parse. + * @param interactive Whether to prompt the user for missing options. + * @returns A list of options. + * + * @note The schema definition are not resolved at this stage. This means that `$ref` will not be resolved, + * and custom keywords like `x-prompt` will not be processed. + */ export async function parseJsonSchemaToOptions( registry: json.schema.SchemaRegistry, schema: json.JsonObject, @@ -106,149 +315,70 @@ export async function parseJsonSchemaToOptions( pointer: json.schema.JsonPointer, parentSchema?: json.JsonObject | json.JsonArray, ) { - if (!parentSchema) { - // Ignore root. - return; - } else if (pointer.split(/\/(?:properties|items|definitions)\//g).length > 2) { - // Ignore subitems (objects or arrays). - return; - } else if (json.isJsonArray(current)) { + if ( + !parentSchema || + json.isJsonArray(current) || + pointer.split(/\/(?:properties|items|definitions)\//g).length > 2 + ) { + // Ignore root, arrays, and subitems. return; } - if (pointer.indexOf('/not/') != -1) { + if (pointer.includes('/not/')) { // We don't support anyOf/not. throw new Error('The "not" keyword is not supported in JSON Schema.'); } const ptr = json.schema.parseJsonPointer(pointer); - const name = ptr[ptr.length - 1]; - - if (ptr[ptr.length - 2] != 'properties') { + if (ptr[ptr.length - 2] !== 'properties') { // Skip any non-property items. return; } + const name = ptr[ptr.length - 1]; - const typeSet = json.schema.getTypesOfSchema(current); - - if (typeSet.size == 0) { - throw new Error('Cannot find type of schema.'); - } - - // We only support number, string or boolean (or array of those), so remove everything else. - const types = [...typeSet].filter((x) => { - switch (x) { - case 'boolean': - case 'number': - case 'string': - return true; - - case 'array': - // Only include arrays if they're boolean, string or number. - if ( - json.isJsonObject(current.items) && - typeof current.items.type == 'string' && - ['boolean', 'number', 'string'].includes(current.items.type) - ) { - return true; - } - - return false; - - case 'object': - return isStringMap(current); + const types = getSupportedTypes(current); - default: - return false; - } - }) as ('string' | 'number' | 'boolean' | 'array' | 'object')[]; - - if (types.length == 0) { + if (types.length === 0) { // This means it's not usable on the command line. e.g. an Object. return; } - // Only keep enum values we support (booleans, numbers and strings). - const enumValues = ((json.isJsonArray(current.enum) && current.enum) || []).filter((x) => { - switch (typeof x) { - case 'boolean': - case 'number': - case 'string': - return true; - - default: - return false; - } - }) as (string | true | number)[]; - - let defaultValue: string | number | boolean | undefined = undefined; - if (current.default !== undefined) { - switch (types[0]) { - case 'string': - if (typeof current.default == 'string') { - defaultValue = current.default; - } - break; - case 'number': - if (typeof current.default == 'number') { - defaultValue = current.default; - } - break; - case 'boolean': - if (typeof current.default == 'boolean') { - defaultValue = current.default; - } - break; - } - } - + const [type] = types; const $default = current.$default; const $defaultIndex = - json.isJsonObject($default) && $default['$source'] == 'argv' ? $default['index'] : undefined; + isJsonObject($default) && $default['$source'] === 'argv' ? $default['index'] : undefined; const positional: number | undefined = - typeof $defaultIndex == 'number' ? $defaultIndex : undefined; + typeof $defaultIndex === 'number' ? $defaultIndex : undefined; - let required = json.isJsonArray(schema.required) ? schema.required.includes(name) : false; + let required = json.isJsonArray(schema.required) && schema.required.includes(name); if (required && interactive && current['x-prompt']) { required = false; } - const alias = json.isJsonArray(current.aliases) - ? [...current.aliases].map((x) => '' + x) - : current.alias - ? ['' + current.alias] - : []; - const format = typeof current.format == 'string' ? current.format : undefined; - const visible = current.visible === undefined || current.visible === true; - const hidden = !!current.hidden || !visible; - - const xUserAnalytics = current['x-user-analytics']; - const userAnalytics = typeof xUserAnalytics === 'string' ? xUserAnalytics : undefined; - - // Deprecated is set only if it's true or a string. + const visible = current.visible !== false; const xDeprecated = current['x-deprecated']; - const deprecated = - xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : undefined; + const enumValues = getEnumValues(current); const option: Option = { name, - description: '' + (current.description === undefined ? '' : current.description), - default: defaultValue, - choices: enumValues.length ? enumValues : undefined, + description: String(current.description ?? ''), + default: getDefaultValue(current, type), + choices: enumValues?.length ? enumValues : undefined, required, - alias, - format, - hidden, - userAnalytics, - deprecated, + alias: getAliases(current), + format: typeof current.format === 'string' ? current.format : undefined, + hidden: !!current.hidden || !visible, + userAnalytics: + typeof current['x-user-analytics'] === 'string' ? current['x-user-analytics'] : undefined, + deprecated: xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : undefined, positional, - ...(types[0] === 'object' + ...(type === 'object' ? { type: 'array', itemValueType: 'string', } : { - type: types[0], + type, }), }; @@ -280,10 +410,10 @@ export function addSchemaOptionsToCommand( localYargs: Argv, options: Option[], includeDefaultValues: boolean, -): Map { +): Map { const booleanOptionsWithNoPrefix = new Set(); const keyValuePairOptions = new Set(); - const optionsWithAnalytics = new Map(); + const optionsWithAnalytics = new Map(); for (const option of options) { const { @@ -309,7 +439,7 @@ export function addSchemaOptionsToCommand( } if (itemValueType) { - keyValuePairOptions.add(name); + keyValuePairOptions.add(dashedName); } const sharedOptions: YargsOptions & PositionalOptions = { @@ -318,7 +448,7 @@ export function addSchemaOptionsToCommand( description, deprecated, choices, - coerce: itemValueType ? coerceToStringMap.bind(null, dashedName) : undefined, + coerce: itemValueType ? coerceToStringMap : undefined, // This should only be done when `--help` is used otherwise default will override options set in angular.json. ...(includeDefaultValues ? { default: defaultVal } : {}), }; @@ -338,10 +468,15 @@ export function addSchemaOptionsToCommand( // Record option of analytics. if (userAnalytics !== undefined) { - optionsWithAnalytics.set(name, userAnalytics); + optionsWithAnalytics.set(name, userAnalytics as EventCustomDimension); } } + // Valid key/value options + if (keyValuePairOptions.size) { + localYargs.check(checkStringMap.bind(null, keyValuePairOptions), false); + } + // Handle options which have been defined in the schema with `no` prefix. if (booleanOptionsWithNoPrefix.size) { localYargs.middleware((options: Arguments) => { diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts index 5ec5db644bef..d311373d69f0 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -6,88 +6,125 @@ * found in the LICENSE file at https://angular.dev/license */ -import { json, schema } from '@angular-devkit/core'; -import yargs, { positional } from 'yargs'; +import { JsonObject, schema } from '@angular-devkit/core'; +import yargs from 'yargs'; import { addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema'; -const YError = (() => { - try { - const y = yargs().strict().fail(false).exitProcess(false).parse(['--forced-failure']); - } catch (e) { - if (!(e instanceof Error)) { - throw new Error('Unexpected non-Error thrown'); - } - - return e.constructor as typeof Error; - } - throw new Error('Expected parse to fail'); -})(); - -interface ParseFunction { - (argv: string[]): unknown; -} - -function withParseForSchema( - jsonSchema: json.JsonObject, - { - interactive = true, - includeDefaultValues = true, - }: { interactive?: boolean; includeDefaultValues?: boolean } = {}, -): ParseFunction { - let actualParse: ParseFunction = () => { - throw new Error('Called before init'); - }; - const parse: ParseFunction = (args) => { - return actualParse(args); - }; - - beforeEach(async () => { - const registry = new schema.CoreSchemaRegistry(); - const options = await parseJsonSchemaToOptions(registry, jsonSchema, interactive); - - actualParse = async (args: string[]) => { - // Create a fresh yargs for each call. The yargs object is stateful and - // calling .parse multiple times on the same instance isn't safe. - const localYargs = yargs().exitProcess(false).strict().fail(false); - addSchemaOptionsToCommand(localYargs, options, includeDefaultValues); - +describe('parseJsonSchemaToOptions', () => { + describe('without required fields in schema', () => { + const parse = async (args: string[]) => { // Yargs only exposes the parse errors as proper errors when using the // callback syntax. This unwraps that ugly workaround so tests can just // use simple .toThrow/.toEqual assertions. return localYargs.parseAsync(args); }; - }); - - return parse; -} -describe('parseJsonSchemaToOptions', () => { - describe('without required fields in schema', () => { - const parse = withParseForSchema({ - 'type': 'object', - 'properties': { - 'maxSize': { - 'type': 'number', - }, - 'ssr': { - 'type': 'string', - 'enum': ['always', 'surprise-me', 'never'], - }, - 'extendable': { - 'type': 'object', - 'properties': {}, - 'additionalProperties': { - 'type': 'string', + let localYargs: yargs.Argv; + beforeEach(async () => { + // Create a fresh yargs for each call. The yargs object is stateful and + // calling .parse multiple times on the same instance isn't safe. + localYargs = yargs().exitProcess(false).strict().fail(false).wrap(1_000); + const jsonSchema = { + 'type': 'object', + 'properties': { + 'maxSize': { + 'type': 'number', }, - }, - 'someDefine': { - 'type': 'object', - 'additionalProperties': { + 'ssr': { 'type': 'string', + 'enum': ['always', 'surprise-me', 'never'], + }, + 'arrayWithChoices': { + 'type': 'array', + 'default': ['default-array'], + 'items': { + 'type': 'string', + 'enum': ['always', 'never', 'default-array'], + }, + }, + 'extendable': { + 'type': 'object', + 'properties': {}, + 'additionalProperties': { + 'type': 'string', + }, + }, + 'someDefine': { + 'type': 'object', + 'additionalProperties': { + 'type': 'string', + }, + }, + 'arrayWithChoicesInOneOf': { + 'type': 'array', + 'items': { + 'oneOf': [ + { + 'enum': ['default', 'verbose'], + }, + { + 'type': 'array', + 'minItems': 1, + 'maxItems': 2, + 'items': [ + { + 'enum': ['default', 'verbose'], + }, + { + 'type': 'object', + }, + ], + }, + ], + }, + }, + 'arrayWithComplexAnyOf': { + 'type': 'array', + 'items': { + 'oneOf': [ + { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'enum': ['default', 'verbose'], + }, + ], + }, + { + 'type': 'array', + 'minItems': 1, + 'maxItems': 2, + 'items': [ + { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'enum': ['default', 'verbose'], + }, + ], + }, + { + 'type': 'object', + }, + ], + }, + ], + }, }, }, - }, + }; + const registry = new schema.CoreSchemaRegistry(); + const options = await parseJsonSchemaToOptions( + registry, + jsonSchema as unknown as JsonObject, + false, + ); + addSchemaOptionsToCommand(localYargs, options, true); }); describe('type=number', () => { @@ -100,6 +137,68 @@ describe('parseJsonSchemaToOptions', () => { }); }); + describe('type=array, enum', () => { + it('parses valid option value', async () => { + expect( + await parse(['--arrayWithChoices', 'always', '--arrayWithChoices', 'never']), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithChoices': ['always', 'never'], + }), + ); + }); + + it('rejects non-enum values', async () => { + await expectAsync(parse(['--arrayWithChoices', 'yes'])).toBeRejectedWithError( + /Argument: array-with-choices, Given: "yes", Choices:/, + ); + }); + + it('should add default value to help', async () => { + expect(await localYargs.getHelp()).toContain('[default: ["default-array"]]'); + }); + }); + + describe('type=array, enum in oneOf', () => { + it('parses valid option value', async () => { + expect( + await parse([ + '--arrayWithChoicesInOneOf', + 'default', + '--arrayWithChoicesInOneOf', + 'verbose', + ]), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithChoicesInOneOf': ['default', 'verbose'], + }), + ); + }); + + it('rejects non-enum values', async () => { + await expectAsync(parse(['--arrayWithChoicesInOneOf', 'yes'])).toBeRejectedWithError( + /Argument: array-with-choices-in-one-of, Given: "yes", Choices:/, + ); + }); + }); + + describe('type=array, anyOf', () => { + it('parses valid option value', async () => { + expect( + await parse([ + '--arrayWithComplexAnyOf', + 'default', + '--arrayWithComplexAnyOf', + 'something-else', + ]), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithComplexAnyOf': ['default', 'something-else'], + }), + ); + }); + }); + describe('type=string, enum', () => { it('parses valid option value', async () => { expect(await parse(['--ssr', 'never'])).toEqual( @@ -125,11 +224,9 @@ describe('parseJsonSchemaToOptions', () => { it('rejects invalid values for string maps', async () => { await expectAsync(parse(['--some-define', 'foo'])).toBeRejectedWithError( - YError, /Invalid value for argument: some-define, Given: 'foo', Expected key=value pair/, ); await expectAsync(parse(['--some-define', '42'])).toBeRejectedWithError( - YError, /Invalid value for argument: some-define, Given: '42', Expected key=value pair/, ); }); @@ -162,43 +259,42 @@ describe('parseJsonSchemaToOptions', () => { describe('with required positional argument', () => { it('marks the required argument as required', async () => { - const jsonSchema = JSON.parse(` - { - "$id": "FakeSchema", - "title": "Fake Schema", - "type": "object", - "required": ["a"], - "properties": { - "b": { - "type": "string", - "description": "b.", - "$default": { - "$source": "argv", - "index": 1 - } + const jsonSchema = { + '$id': 'FakeSchema', + 'title': 'Fake Schema', + 'type': 'object', + 'required': ['a'], + 'properties': { + 'b': { + 'type': 'string', + 'description': 'b.', + '$default': { + '$source': 'argv', + 'index': 1, + }, }, - "a": { - "type": "string", - "description": "a.", - "$default": { - "$source": "argv", - "index": 0 - } + 'a': { + 'type': 'string', + 'description': 'a.', + '$default': { + '$source': 'argv', + 'index': 0, + }, }, - "optC": { - "type": "string", - "description": "optC" + 'optC': { + 'type': 'string', + 'description': 'optC', }, - "optA": { - "type": "string", - "description": "optA" + 'optA': { + 'type': 'string', + 'description': 'optA', + }, + 'optB': { + 'type': 'string', + 'description': 'optB', }, - "optB": { - "type": "string", - "description": "optB" - } - } - }`) as json.JsonObject; + }, + }; const registry = new schema.CoreSchemaRegistry(); const options = await parseJsonSchemaToOptions(registry, jsonSchema, /* interactive= */ true); diff --git a/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts index e4b805f1a367..25b723c467a2 100644 --- a/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts +++ b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts @@ -20,7 +20,10 @@ import { assertIsError } from '../../utilities/error'; */ const schematicRedirectVariable = process.env['NG_SCHEMATIC_REDIRECT']?.toLowerCase(); -function shouldWrapSchematic(schematicFile: string, schematicEncapsulation: boolean): boolean { +function shouldWrapSchematic( + schematicFile: string, + schematicEncapsulation: boolean | undefined, +): boolean { // Check environment variable if present switch (schematicRedirectVariable) { case '0': @@ -52,12 +55,12 @@ function shouldWrapSchematic(schematicFile: string, schematicEncapsulation: bool // Check for first-party Angular schematic packages // Angular schematics are safe to use in the wrapped VM context - if (/\/node_modules\/@(?:angular|schematics|nguniversal)\//.test(normalizedSchematicFile)) { - return true; - } + const isFirstParty = /\/node_modules\/@(?:angular|schematics|nguniversal)\//.test( + normalizedSchematicFile, + ); - // Otherwise use the value of the schematic collection's encapsulation option (current default of false) - return schematicEncapsulation; + // Use value of defined option if present, otherwise default to first-party usage. + return schematicEncapsulation ?? isFirstParty; } export class SchematicEngineHost extends NodeModulesEngineHost { @@ -73,7 +76,7 @@ export class SchematicEngineHost extends NodeModulesEngineHost { const referenceRequire = createRequire(__filename); const schematicFile = referenceRequire.resolve(fullPath, { paths: [parentPath] }); - if (shouldWrapSchematic(schematicFile, !!collectionDescription?.encapsulation)) { + if (shouldWrapSchematic(schematicFile, collectionDescription?.encapsulation)) { const schematicPath = dirname(schematicFile); const moduleCache = new Map(); diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index adaf980cd4b3..1ea2fff699c6 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools'; -import { Listr, color, figures } from 'listr2'; +import { Listr, ListrRenderer, ListrTaskWrapper, color, figures } from 'listr2'; import assert from 'node:assert'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; @@ -28,6 +27,7 @@ import { assertIsError } from '../../utilities/error'; import { NgAddSaveDependency, PackageManifest, + PackageMetadata, fetchPackageManifest, fetchPackageMetadata, } from '../../utilities/package-metadata'; @@ -49,9 +49,18 @@ interface AddCommandTaskContext { savePackage?: NgAddSaveDependency; collectionName?: string; executeSchematic: AddCommandModule['executeSchematic']; - hasMismatchedPeer: AddCommandModule['hasMismatchedPeer']; + getPeerDependencyConflicts: AddCommandModule['getPeerDependencyConflicts']; + dryRun?: boolean; + hasSchematics?: boolean; + homepage?: string; } +type AddCommandTaskWrapper = ListrTaskWrapper< + AddCommandTaskContext, + typeof ListrRenderer, + typeof ListrRenderer +>; + /** * The set of packages that should have certain versions excluded from consideration * when attempting to find a compatible version for a package. @@ -64,6 +73,19 @@ const packageVersionExclusions: Record = { '@angular/material': '7.x', }; +const DEFAULT_CONFLICT_DISPLAY_LIMIT = 5; + +/** + * A map of packages to built-in schematics. + * This is used for packages that do not have a native `ng-add` schematic. + */ +const BUILT_IN_SCHEMATICS = { + tailwindcss: { + collection: '@schematics/angular', + name: 'tailwind', + }, +} as const; + export default class AddCommandModule extends SchematicsCommandModule implements CommandModuleImplementation @@ -74,6 +96,7 @@ export default class AddCommandModule protected override allowPrivateSchematics = true; private readonly schematicName = 'ng-add'; private rootRequire = createRequire(this.context.root + '/'); + #projectVersionCache = new Map(); override async builder(argv: Argv): Promise> { const localYargs = (await super.builder(argv)) @@ -121,10 +144,10 @@ export default class AddCommandModule return localYargs; } - // eslint-disable-next-line max-lines-per-function async run(options: Options & OtherOptions): Promise { - const { logger, packageManager } = this.context; - const { verbose, registry, collection, skipConfirmation } = options; + this.#projectVersionCache.clear(); + const { logger } = this.context; + const { collection, skipConfirmation } = options; let packageIdentifier; try { @@ -153,219 +176,117 @@ export default class AddCommandModule const taskContext: AddCommandTaskContext = { packageIdentifier, executeSchematic: this.executeSchematic.bind(this), - hasMismatchedPeer: this.hasMismatchedPeer.bind(this), + getPeerDependencyConflicts: this.getPeerDependencyConflicts.bind(this), + dryRun: options.dryRun, }; - const tasks = new Listr([ - { - title: 'Determining Package Manager', - task(context, task) { - context.usingYarn = packageManager.name === PackageManager.Yarn; - task.output = `Using package manager: ${color.dim(packageManager.name)}`; + const tasks = new Listr( + [ + { + title: 'Determining Package Manager', + task: (context, task) => this.determinePackageManagerTask(context, task), + rendererOptions: { persistentOutput: true }, }, - rendererOptions: { persistentOutput: true }, - }, - { - title: 'Searching for compatible package version', - enabled: packageIdentifier.type === 'range' && packageIdentifier.rawSpec === '*', - async task(context, task) { - assert( - context.packageIdentifier.name, - 'Registry package identifiers should always have a name.', - ); - - // only package name provided; search for viable version - // plus special cases for packages that did not have peer deps setup - let packageMetadata; - try { - packageMetadata = await fetchPackageMetadata(context.packageIdentifier.name, logger, { - registry, - usingYarn: context.usingYarn, - verbose, - }); - } catch (e) { - assertIsError(e); - throw new CommandError( - `Unable to load package information from registry: ${e.message}`, - ); - } - - // Start with the version tagged as `latest` if it exists - const latestManifest = packageMetadata.tags['latest']; - if (latestManifest) { - context.packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); - } - - // Adjust the version based on name and peer dependencies - if ( - latestManifest?.peerDependencies && - Object.keys(latestManifest.peerDependencies).length === 0 - ) { - task.output = `Found compatible package version: ${color.blue(latestManifest.version)}.`; - } else if (!latestManifest || (await context.hasMismatchedPeer(latestManifest))) { - // 'latest' is invalid so search for most recent matching package - - // Allow prelease versions if the CLI itself is a prerelease - const allowPrereleases = prerelease(VERSION.full); - - const versionExclusions = packageVersionExclusions[packageMetadata.name]; - const versionManifests = Object.values(packageMetadata.versions).filter( - (value: PackageManifest) => { - // Prerelease versions are not stable and should not be considered by default - if (!allowPrereleases && prerelease(value.version)) { - return false; - } - // Deprecated versions should not be used or considered - if (value.deprecated) { - return false; - } - // Excluded package versions should not be considered - if ( - versionExclusions && - satisfies(value.version, versionExclusions, { includePrerelease: true }) - ) { - return false; - } - - return true; - }, - ); - - // Sort in reverse SemVer order so that the newest compatible version is chosen - versionManifests.sort((a, b) => compare(b.version, a.version, true)); - - let found = false; - for (const versionManifest of versionManifests) { - const mismatch = await context.hasMismatchedPeer(versionManifest); - if (mismatch) { - continue; - } - - context.packageIdentifier = npa.resolve( - versionManifest.name, - versionManifest.version, - ); - found = true; - break; + { + title: 'Searching for compatible package version', + enabled: packageIdentifier.type === 'range' && packageIdentifier.rawSpec === '*', + task: (context, task) => this.findCompatiblePackageVersionTask(context, task, options), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Loading package information from registry', + task: (context, task) => this.loadPackageInfoTask(context, task, options), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Confirming installation', + enabled: !skipConfirmation && !options.dryRun, + task: (context, task) => this.confirmInstallationTask(context, task), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Installing package', + skip: (context) => { + if (context.dryRun) { + return `Skipping package installation. Would install package ${color.blue( + context.packageIdentifier.toString(), + )}.`; } - if (!found) { - task.output = "Unable to find compatible package. Using 'latest' tag."; - } else { - task.output = `Found compatible package version: ${color.blue(context.packageIdentifier.toString())}.`; - } - } else { - task.output = `Found compatible package version: ${color.blue(context.packageIdentifier.toString())}.`; - } + return false; + }, + task: (context, task) => this.installPackageTask(context, task, options), + rendererOptions: { bottomBar: Infinity }, }, - rendererOptions: { persistentOutput: true }, - }, + // TODO: Rework schematic execution as a task and insert here + ], { - title: 'Loading package information from registry', - async task(context, task) { - let manifest; - try { - manifest = await fetchPackageManifest(context.packageIdentifier.toString(), logger, { - registry, - verbose, - usingYarn: context.usingYarn, - }); - } catch (e) { - assertIsError(e); - throw new CommandError( - `Unable to fetch package information for '${context.packageIdentifier}': ${e.message}`, - ); - } + /* options */ + }, + ); + + try { + const result = await tasks.run(taskContext); + assert(result.collectionName, 'Collection name should always be available'); + + // Check if the installed package has actual add actions and not just schematic support + if (result.hasSchematics && !options.dryRun) { + const workflow = this.getOrCreateWorkflowForBuilder(result.collectionName); + const collection = workflow.engine.createCollection(result.collectionName); - context.savePackage = manifest['ng-add']?.save; - context.collectionName = manifest.name; + // listSchematicNames cannot be used here since it does not list private schematics. + // Most `ng-add` schematics are marked as private. + // TODO: Consider adding a `hasSchematic` helper to the schematic collection object. + try { + collection.createSchematic(this.schematicName, true); + } catch { + result.hasSchematics = false; + } + } - if (await context.hasMismatchedPeer(manifest)) { - task.output = color.yellow( - figures.warning + - ' Package has unmet peer dependencies. Adding the package may not succeed.', + if (!result.hasSchematics) { + // Fallback to a built-in schematic if the package does not have an `ng-add` schematic + const packageName = result.packageIdentifier.name; + if (packageName) { + const builtInSchematic = + BUILT_IN_SCHEMATICS[packageName as keyof typeof BUILT_IN_SCHEMATICS]; + if (builtInSchematic) { + logger.info( + `The ${color.blue(packageName)} package does not provide \`ng add\` actions.`, ); - } - }, - rendererOptions: { persistentOutput: true }, - }, - { - title: 'Confirming installation', - enabled: !skipConfirmation, - async task(context, task) { - if (!isTTY()) { - task.output = - `'--skip-confirmation' can be used to bypass installation confirmation. ` + - `Ensure package name is correct prior to '--skip-confirmation' option usage.`; - throw new CommandError('No terminal detected'); - } + logger.info('The Angular CLI will use built-in actions to add it to your project.'); - const { ListrInquirerPromptAdapter } = await import('@listr2/prompt-adapter-inquirer'); - const { confirm } = await import('@inquirer/prompts'); - const shouldProceed = await task.prompt(ListrInquirerPromptAdapter).run(confirm, { - message: - `The package ${color.blue(context.packageIdentifier.toString())} will be installed and executed.\n` + - 'Would you like to proceed?', - default: true, - theme: { prefix: '' }, - }); - - if (!shouldProceed) { - throw new CommandError('Command aborted'); + return this.executeSchematic({ + ...options, + collection: builtInSchematic.collection, + schematicName: builtInSchematic.name, + }); } - }, - rendererOptions: { persistentOutput: true }, - }, - { - async task(context, task) { - // Only show if installation will actually occur - task.title = 'Installing package'; - - if (context.savePackage === false) { - task.title += ' in temporary location'; - - // Temporary packages are located in a different directory - // Hence we need to resolve them using the temp path - const { success, tempNodeModules } = await packageManager.installTemp( - context.packageIdentifier.toString(), - registry ? [`--registry="${registry}"`] : undefined, - ); - const tempRequire = createRequire(tempNodeModules + '/'); - assert(context.collectionName, 'Collection name should always be available'); - const resolvedCollectionPath = tempRequire.resolve( - join(context.collectionName, 'package.json'), - ); + } - if (!success) { - throw new CommandError('Unable to install package'); - } + let message = options.dryRun + ? 'The package does not provide any `ng add` actions, so no further actions would be taken.' + : 'Package installed successfully. The package does not provide any `ng add` actions, so no further actions were taken.'; - context.collectionName = dirname(resolvedCollectionPath); - } else { - const success = await packageManager.install( - context.packageIdentifier.toString(), - context.savePackage, - registry ? [`--registry="${registry}"`] : undefined, - undefined, - ); + if (result.homepage) { + message += `\nFor more information about this package, visit its homepage at ${result.homepage}`; + } + logger.info(message); - if (!success) { - throw new CommandError('Unable to install package'); - } - } - }, - rendererOptions: { bottomBar: Infinity }, - }, - // TODO: Rework schematic execution as a task and insert here - ]); + return; + } - try { - const result = await tasks.run(taskContext); - assert(result.collectionName, 'Collection name should always be available'); + if (options.dryRun) { + logger.info("The package's `ng add` actions would be executed next."); + + return; + } return this.executeSchematic({ ...options, collection: result.collectionName }); } catch (e) { if (e instanceof CommandError) { + logger.error(e.message); + return 1; } @@ -373,6 +294,239 @@ export default class AddCommandModule } } + private determinePackageManagerTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + ): void { + const { packageManager } = this.context; + context.usingYarn = packageManager.name === PackageManager.Yarn; + task.output = `Using package manager: ${color.dim(packageManager.name)}`; + } + + private async findCompatiblePackageVersionTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + options: Options, + ): Promise { + const { logger } = this.context; + const { verbose, registry } = options; + + assert( + context.packageIdentifier.name, + 'Registry package identifiers should always have a name.', + ); + + // only package name provided; search for viable version + // plus special cases for packages that did not have peer deps setup + let packageMetadata; + try { + packageMetadata = await fetchPackageMetadata(context.packageIdentifier.name, logger, { + registry, + usingYarn: context.usingYarn, + verbose, + }); + } catch (e) { + assertIsError(e); + throw new CommandError(`Unable to load package information from registry: ${e.message}`); + } + + const rejectionReasons: string[] = []; + + // Start with the version tagged as `latest` if it exists + const latestManifest = packageMetadata.tags['latest']; + if (latestManifest) { + const latestConflicts = await this.getPeerDependencyConflicts(latestManifest); + if (latestConflicts) { + // 'latest' is invalid so search for most recent matching package + rejectionReasons.push(...latestConflicts); + } else { + context.packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); + task.output = `Found compatible package version: ${color.blue(latestManifest.version)}.`; + + return; + } + } + + // Allow prelease versions if the CLI itself is a prerelease + const allowPrereleases = !!prerelease(VERSION.full); + const versionManifests = this.#getPotentialVersionManifests(packageMetadata, allowPrereleases); + + let found = false; + for (const versionManifest of versionManifests) { + // Already checked the 'latest' version + if (latestManifest?.version === versionManifest.version) { + continue; + } + + const conflicts = await this.getPeerDependencyConflicts(versionManifest); + if (conflicts) { + if (options.verbose || rejectionReasons.length < DEFAULT_CONFLICT_DISPLAY_LIMIT) { + rejectionReasons.push(...conflicts); + } + continue; + } + + context.packageIdentifier = npa.resolve(versionManifest.name, versionManifest.version); + found = true; + break; + } + + if (!found) { + let message = `Unable to find compatible package. Using 'latest' tag.`; + if (rejectionReasons.length > 0) { + message += + '\nThis is often because of incompatible peer dependencies.\n' + + 'These versions were rejected due to the following conflicts:\n' + + rejectionReasons + .slice(0, options.verbose ? undefined : DEFAULT_CONFLICT_DISPLAY_LIMIT) + .map((r) => ` - ${r}`) + .join('\n'); + } + task.output = message; + } else { + task.output = `Found compatible package version: ${color.blue( + context.packageIdentifier.toString(), + )}.`; + } + } + + #getPotentialVersionManifests( + packageMetadata: PackageMetadata, + allowPrereleases: boolean, + ): PackageManifest[] { + const versionExclusions = packageVersionExclusions[packageMetadata.name]; + const versionManifests = Object.values(packageMetadata.versions).filter( + (value: PackageManifest) => { + // Prerelease versions are not stable and should not be considered by default + if (!allowPrereleases && prerelease(value.version)) { + return false; + } + // Deprecated versions should not be used or considered + if (value.deprecated) { + return false; + } + // Excluded package versions should not be considered + if ( + versionExclusions && + satisfies(value.version, versionExclusions, { includePrerelease: true }) + ) { + return false; + } + + return true; + }, + ); + + // Sort in reverse SemVer order so that the newest compatible version is chosen + return versionManifests.sort((a, b) => compare(b.version, a.version, true)); + } + + private async loadPackageInfoTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + options: Options, + ): Promise { + const { logger } = this.context; + const { verbose, registry } = options; + + let manifest; + try { + manifest = await fetchPackageManifest(context.packageIdentifier.toString(), logger, { + registry, + verbose, + usingYarn: context.usingYarn, + }); + } catch (e) { + assertIsError(e); + throw new CommandError( + `Unable to fetch package information for '${context.packageIdentifier}': ${e.message}`, + ); + } + + context.hasSchematics = !!manifest.schematics; + context.savePackage = manifest['ng-add']?.save; + context.collectionName = manifest.name; + context.homepage = manifest.homepage; + + if (await this.getPeerDependencyConflicts(manifest)) { + task.output = color.yellow( + figures.warning + + ' Package has unmet peer dependencies. Adding the package may not succeed.', + ); + } + } + + private async confirmInstallationTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + ): Promise { + if (!isTTY()) { + task.output = + `'--skip-confirmation' can be used to bypass installation confirmation. ` + + `Ensure package name is correct prior to '--skip-confirmation' option usage.`; + throw new CommandError('No terminal detected'); + } + + const { ListrInquirerPromptAdapter } = await import('@listr2/prompt-adapter-inquirer'); + const { confirm } = await import('@inquirer/prompts'); + const shouldProceed = await task.prompt(ListrInquirerPromptAdapter).run(confirm, { + message: + `The package ${color.blue(context.packageIdentifier.toString())} will be installed and executed.\n` + + 'Would you like to proceed?', + default: true, + theme: { prefix: '' }, + }); + + if (!shouldProceed) { + throw new CommandError('Command aborted'); + } + } + + private async installPackageTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + options: Options, + ): Promise { + const { packageManager } = this.context; + const { registry } = options; + + // Only show if installation will actually occur + task.title = 'Installing package'; + + if (context.savePackage === false) { + task.title += ' in temporary location'; + + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const { success, tempNodeModules } = await packageManager.installTemp( + context.packageIdentifier.toString(), + registry ? [`--registry="${registry}"`] : undefined, + ); + const tempRequire = createRequire(tempNodeModules + '/'); + assert(context.collectionName, 'Collection name should always be available'); + const resolvedCollectionPath = tempRequire.resolve( + join(context.collectionName, 'package.json'), + ); + + if (!success) { + throw new CommandError('Unable to install package'); + } + + context.collectionName = dirname(resolvedCollectionPath); + } else { + const success = await packageManager.install( + context.packageIdentifier.toString(), + context.savePackage, + registry ? [`--registry="${registry}"`] : undefined, + undefined, + ); + + if (!success) { + throw new CommandError('Unable to install package'); + } + } + } + private async isProjectVersionValid(packageIdentifier: npa.Result): Promise { if (!packageIdentifier.name) { return false; @@ -441,49 +595,42 @@ export default class AddCommandModule return false; } - private async executeSchematic( - options: Options & OtherOptions, + private executeSchematic( + options: Options & OtherOptions & { schematicName?: string }, ): Promise { - try { - const { - verbose, - skipConfirmation, + const { + verbose, + skipConfirmation, + interactive, + force, + dryRun, + registry, + defaults, + collection: collectionName, + schematicName, + ...schematicOptions + } = options; + + return this.runSchematic({ + schematicOptions, + schematicName: schematicName ?? this.schematicName, + collectionName, + executionOptions: { interactive, force, dryRun, - registry, defaults, - collection: collectionName, - ...schematicOptions - } = options; - - return await this.runSchematic({ - schematicOptions, - schematicName: this.schematicName, - collectionName, - executionOptions: { - interactive, - force, - dryRun, - defaults, - packageRegistry: registry, - }, - }); - } catch (e) { - if (e instanceof NodePackageDoesNotSupportSchematics) { - this.context.logger.error( - 'The package that you are trying to add does not support schematics.' + - 'You can try using a different version of the package or contact the package author to add ng-add support.', - ); - - return 1; - } - - throw e; - } + packageRegistry: registry, + }, + }); } private async findProjectVersion(name: string): Promise { + const cachedVersion = this.#projectVersionCache.get(name); + if (cachedVersion !== undefined) { + return cachedVersion; + } + const { logger, root } = this.context; let installedPackage; try { @@ -493,6 +640,7 @@ export default class AddCommandModule if (installedPackage) { try { const installed = await fetchPackageManifest(dirname(installedPackage), logger); + this.#projectVersionCache.set(name, installed.version); return installed.version; } catch {} @@ -507,48 +655,63 @@ export default class AddCommandModule const version = projectManifest.dependencies?.[name] || projectManifest.devDependencies?.[name]; if (version) { + this.#projectVersionCache.set(name, version); + return version; } } + this.#projectVersionCache.set(name, null); + return null; } - private async hasMismatchedPeer(manifest: PackageManifest): Promise { - for (const peer in manifest.peerDependencies) { + private async getPeerDependencyConflicts(manifest: PackageManifest): Promise { + if (!manifest.peerDependencies) { + return false; + } + + const checks = Object.entries(manifest.peerDependencies).map(async ([peer, range]) => { let peerIdentifier; try { - peerIdentifier = npa.resolve(peer, manifest.peerDependencies[peer]); + peerIdentifier = npa.resolve(peer, range); } catch { this.context.logger.warn(`Invalid peer dependency ${peer} found in package.`); - continue; + + return null; } - if (peerIdentifier.type === 'version' || peerIdentifier.type === 'range') { - try { - const version = await this.findProjectVersion(peer); - if (!version) { - continue; - } + if (peerIdentifier.type !== 'version' && peerIdentifier.type !== 'range') { + // type === 'tag' | 'file' | 'directory' | 'remote' | 'git' + // Cannot accurately compare these as the tag/location may have changed since install. + return null; + } - const options = { includePrerelease: true }; + try { + const version = await this.findProjectVersion(peer); + if (!version) { + return null; + } - if ( - !intersects(version, peerIdentifier.rawSpec, options) && - !satisfies(version, peerIdentifier.rawSpec, options) - ) { - return true; - } - } catch { - // Not found or invalid so ignore - continue; + const options = { includePrerelease: true }; + if ( + !intersects(version, peerIdentifier.rawSpec, options) && + !satisfies(version, peerIdentifier.rawSpec, options) + ) { + return ( + `Package "${manifest.name}@${manifest.version}" has an incompatible peer dependency to "` + + `${peer}@${peerIdentifier.rawSpec}" (requires "${version}" in project).` + ); } - } else { - // type === 'tag' | 'file' | 'directory' | 'remote' | 'git' - // Cannot accurately compare these as the tag/location may have changed since install + } catch { + // Not found or invalid so ignore } - } - return false; + return null; + }); + + const conflicts = (await Promise.all(checks)).filter((result): result is string => !!result); + + return conflicts.length > 0 && conflicts; } } diff --git a/packages/angular/cli/src/commands/cache/info/cli.ts b/packages/angular/cli/src/commands/cache/info/cli.ts index 447d92e02c1f..f4278d52db74 100644 --- a/packages/angular/cli/src/commands/cache/info/cli.ts +++ b/packages/angular/cli/src/commands/cache/info/cli.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { tags } from '@angular-devkit/core'; import * as fs from 'node:fs/promises'; import { join } from 'node:path'; import { Argv } from 'yargs'; @@ -15,6 +14,7 @@ import { CommandModuleImplementation, CommandScope, } from '../../../command-builder/command-module'; +import { colors } from '../../../utilities/color'; import { isCI } from '../../../utilities/environment-options'; import { getCacheConfig } from '../utilities'; @@ -29,15 +29,44 @@ export class CacheInfoCommandModule extends CommandModule implements CommandModu } async run(): Promise { - const { path, environment, enabled } = getCacheConfig(this.context.workspace); - - this.context.logger.info(tags.stripIndents` - Enabled: ${enabled ? 'yes' : 'no'} - Environment: ${environment} - Path: ${path} - Size on disk: ${await this.getSizeOfDirectory(path)} - Effective status on current machine: ${this.effectiveEnabledStatus() ? 'enabled' : 'disabled'} - `); + const cacheConfig = getCacheConfig(this.context.workspace); + const { path, environment, enabled } = cacheConfig; + + const effectiveStatus = this.effectiveEnabledStatus(cacheConfig); + const sizeOnDisk = await this.getSizeOfDirectory(path); + + const info: { label: string; value: string }[] = [ + { + label: 'Enabled', + value: enabled ? colors.green('Yes') : colors.red('No'), + }, + { + label: 'Environment', + value: colors.cyan(environment), + }, + { + label: 'Path', + value: colors.cyan(path), + }, + { + label: 'Size on disk', + value: colors.cyan(sizeOnDisk), + }, + { + label: 'Effective Status', + value: + (effectiveStatus ? colors.green('Enabled') : colors.red('Disabled')) + + ' (current machine)', + }, + ]; + + const maxLabelLength = Math.max(...info.map((l) => l.label.length)); + + const output = info + .map(({ label, value }) => colors.bold(label.padEnd(maxLabelLength + 2)) + `: ${value}`) + .join('\n'); + + this.context.logger.info(`\n${colors.bold('Cache Information')}\n\n${output}\n`); } private async getSizeOfDirectory(path: string): Promise { @@ -82,8 +111,8 @@ export class CacheInfoCommandModule extends CommandModule implements CommandModu return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`; } - private effectiveEnabledStatus(): boolean { - const { enabled, environment } = getCacheConfig(this.context.workspace); + private effectiveEnabledStatus(cacheConfig: { enabled: boolean; environment: string }): boolean { + const { enabled, environment } = cacheConfig; if (enabled) { switch (environment) { diff --git a/packages/angular/cli/src/commands/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts index 7e3618eeb17e..0076752ac6f3 100644 --- a/packages/angular/cli/src/commands/mcp/cli.ts +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -7,10 +7,13 @@ */ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { Argv } from 'yargs'; -import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import type { Argv } from 'yargs'; +import { + CommandModule, + type CommandModuleImplementation, +} from '../../command-builder/command-module'; import { isTTY } from '../../utilities/tty'; -import { createMcpServer } from './mcp-server'; +import { EXPERIMENTAL_TOOLS, createMcpServer } from './mcp-server'; const INTERACTIVE_MESSAGE = ` To start using the Angular CLI MCP Server, add this configuration to your host: @@ -25,6 +28,8 @@ To start using the Angular CLI MCP Server, add this configuration to your host: } Exact configuration may differ depending on the host. + +For more information and documentation, visit: https://angular.dev/ai/mcp `; export default class McpCommandModule extends CommandModule implements CommandModuleImplementation { @@ -49,6 +54,8 @@ export default class McpCommandModule extends CommandModule implements CommandMo alias: 'E', array: true, describe: 'Enable an experimental tool.', + choices: EXPERIMENTAL_TOOLS.map(({ name }) => name), + hidden: true, }); } diff --git a/packages/angular/cli/src/commands/mcp/constants.ts b/packages/angular/cli/src/commands/mcp/constants.ts index 6530bfd34175..789820ca2f53 100644 --- a/packages/angular/cli/src/commands/mcp/constants.ts +++ b/packages/angular/cli/src/commands/mcp/constants.ts @@ -7,7 +7,7 @@ */ export const k1 = '@angular/cli'; -export const at = 'QBHBbOdEO4CmBOC2d7jNmg=='; +export const at = 'gv2tkIHTOiWtI6Su96LXLQ=='; export const iv = Buffer.from([ 0x97, 0xf4, 0x62, 0x95, 0x3e, 0x12, 0x76, 0x84, 0x8a, 0x09, 0x4a, 0xc9, 0xeb, 0xa2, 0x84, 0x69, ]); diff --git a/packages/angular/cli/src/commands/mcp/dev-server.ts b/packages/angular/cli/src/commands/mcp/dev-server.ts new file mode 100644 index 000000000000..e6da33aa7bd3 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/dev-server.ts @@ -0,0 +1,148 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { ChildProcess } from 'child_process'; +import type { Host } from './host'; + +// Log messages that we want to catch to identify the build status. + +const BUILD_SUCCEEDED_MESSAGE = 'Application bundle generation complete.'; +const BUILD_FAILED_MESSAGE = 'Application bundle generation failed.'; +const WAITING_FOR_CHANGES_MESSAGE = 'Watch mode enabled. Watching for file changes...'; +const CHANGES_DETECTED_START_MESSAGE = '❯ Changes detected. Rebuilding...'; +const CHANGES_DETECTED_SUCCESS_MESSAGE = '✔ Changes detected. Rebuilding...'; + +const BUILD_START_MESSAGES = [CHANGES_DETECTED_START_MESSAGE]; +const BUILD_END_MESSAGES = [ + BUILD_SUCCEEDED_MESSAGE, + BUILD_FAILED_MESSAGE, + WAITING_FOR_CHANGES_MESSAGE, + CHANGES_DETECTED_SUCCESS_MESSAGE, +]; + +export type BuildStatus = 'success' | 'failure' | 'unknown'; + +/** + * An Angular development server managed by the MCP server. + */ +export interface DevServer { + /** + * Launches the dev server and returns immediately. + * + * Throws if this server is already running. + */ + start(): void; + + /** + * If the dev server is running, stops it. + */ + stop(): void; + + /** + * Gets all the server logs so far (stdout + stderr). + */ + getServerLogs(): string[]; + + /** + * Gets all the server logs from the latest build. + */ + getMostRecentBuild(): { status: BuildStatus; logs: string[] }; + + /** + * Whether the dev server is currently being built, or is awaiting further changes. + */ + isBuilding(): boolean; + + /** + * `ng serve` port to use. + */ + port: number; +} + +export function devServerKey(project?: string) { + return project ?? ''; +} + +/** + * A local Angular development server managed by the MCP server. + */ +export class LocalDevServer implements DevServer { + readonly host: Host; + readonly port: number; + readonly project?: string; + + private devServerProcess: ChildProcess | null = null; + private serverLogs: string[] = []; + private buildInProgress = false; + private latestBuildLogStartIndex?: number = undefined; + private latestBuildStatus: BuildStatus = 'unknown'; + + constructor({ host, port, project }: { host: Host; port: number; project?: string }) { + this.host = host; + this.project = project; + this.port = port; + } + + start() { + if (this.devServerProcess) { + throw Error('Dev server already started.'); + } + + const args = ['serve']; + if (this.project) { + args.push(this.project); + } + + args.push(`--port=${this.port}`); + + this.devServerProcess = this.host.spawn('ng', args, { stdio: 'pipe' }); + this.devServerProcess.stdout?.on('data', (data) => { + this.addLog(data.toString()); + }); + this.devServerProcess.stderr?.on('data', (data) => { + this.addLog(data.toString()); + }); + this.devServerProcess.stderr?.on('close', () => { + this.stop(); + }); + this.buildInProgress = true; + } + + private addLog(log: string) { + this.serverLogs.push(log); + + if (BUILD_START_MESSAGES.some((message) => log.startsWith(message))) { + this.buildInProgress = true; + this.latestBuildLogStartIndex = this.serverLogs.length - 1; + } else if (BUILD_END_MESSAGES.some((message) => log.startsWith(message))) { + this.buildInProgress = false; + // We consider everything except a specific failure message to be a success. + this.latestBuildStatus = log.startsWith(BUILD_FAILED_MESSAGE) ? 'failure' : 'success'; + } + } + + stop() { + this.devServerProcess?.kill(); + this.devServerProcess = null; + } + + getServerLogs(): string[] { + return [...this.serverLogs]; + } + + getMostRecentBuild() { + return { + status: this.latestBuildStatus, + logs: this.serverLogs.slice(this.latestBuildLogStartIndex), + }; + } + + isBuilding() { + return this.buildInProgress; + } +} diff --git a/packages/angular/cli/src/commands/mcp/host.ts b/packages/angular/cli/src/commands/mcp/host.ts new file mode 100644 index 000000000000..0be6ff67a7fc --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/host.ts @@ -0,0 +1,198 @@ +/** + * @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 + */ + +/** + * @fileoverview + * This file defines an abstraction layer for operating-system or file-system operations, such as + * command execution. This allows for easier testing by enabling the injection of mock or + * test-specific implementations. + */ + +import { existsSync as nodeExistsSync } from 'fs'; +import { ChildProcess, spawn } from 'node:child_process'; +import { Stats } from 'node:fs'; +import { stat } from 'node:fs/promises'; +import { createServer } from 'node:net'; + +/** + * An error thrown when a command fails to execute. + */ +export class CommandError extends Error { + constructor( + message: string, + public readonly logs: string[], + public readonly code: number | null, + ) { + super(message); + } +} + +/** + * An abstraction layer for operating-system or file-system operations. + */ +export interface Host { + /** + * Gets the stats of a file or directory. + * @param path The path to the file or directory. + * @returns A promise that resolves to the stats. + */ + stat(path: string): Promise; + + /** + * Checks if a path exists on the file system. + * @param path The path to check. + * @returns A boolean indicating whether the path exists. + */ + existsSync(path: string): boolean; + + /** + * Spawns a child process and returns a promise that resolves with the process's + * output or rejects with a structured error. + * @param command The command to run. + * @param args The arguments to pass to the command. + * @param options Options for the child process. + * @returns A promise that resolves with the standard output and standard error of the command. + */ + runCommand( + command: string, + args: readonly string[], + options?: { + timeout?: number; + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + }, + ): Promise<{ logs: string[] }>; + + /** + * Spawns a long-running child process and returns the `ChildProcess` object. + * @param command The command to run. + * @param args The arguments to pass to the command. + * @param options Options for the child process. + * @returns The spawned `ChildProcess` instance. + */ + spawn( + command: string, + args: readonly string[], + options?: { + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + }, + ): ChildProcess; + + /** + * Finds an available TCP port on the system. + */ + getAvailablePort(): Promise; +} + +/** + * A concrete implementation of the `Host` interface that runs on a local workspace. + */ +export const LocalWorkspaceHost: Host = { + stat, + + existsSync: nodeExistsSync, + + runCommand: async ( + command: string, + args: readonly string[], + options: { + timeout?: number; + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + } = {}, + ): Promise<{ logs: string[] }> => { + const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined; + + return new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { + shell: false, + stdio: options.stdio ?? 'pipe', + signal, + cwd: options.cwd, + env: { + ...process.env, + ...options.env, + }, + }); + + const logs: string[] = []; + childProcess.stdout?.on('data', (data) => logs.push(data.toString())); + childProcess.stderr?.on('data', (data) => logs.push(data.toString())); + + childProcess.on('close', (code) => { + if (code === 0) { + resolve({ logs }); + } else { + const message = `Process exited with code ${code}.`; + reject(new CommandError(message, logs, code)); + } + }); + + childProcess.on('error', (err) => { + if (err.name === 'AbortError') { + const message = `Process timed out.`; + reject(new CommandError(message, logs, null)); + + return; + } + const message = `Process failed with error: ${err.message}`; + reject(new CommandError(message, logs, null)); + }); + }); + }, + + spawn( + command: string, + args: readonly string[], + options: { + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + } = {}, + ): ChildProcess { + return spawn(command, args, { + shell: false, + stdio: options.stdio ?? 'pipe', + cwd: options.cwd, + env: { + ...process.env, + ...options.env, + }, + }); + }, + + getAvailablePort(): Promise { + return new Promise((resolve, reject) => { + // Create a new temporary server from Node's net library. + const server = createServer(); + + server.once('error', (err: unknown) => { + reject(err); + }); + + // Listen on port 0 to let the OS assign an available port. + server.listen(0, () => { + const address = server.address(); + + // Ensure address is an object with a port property. + if (address && typeof address === 'object') { + const port = address.port; + + server.close(); + resolve(port); + } else { + reject(new Error('Unable to retrieve address information from server.')); + } + }); + }); + }, +}; diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index ceedc6374ad6..dfcd162a44f7 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -7,28 +7,47 @@ */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import path from 'node:path'; +import { join } from 'node:path'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; +import type { DevServer } from './dev-server'; import { registerInstructionsResource } from './resources/instructions'; +import { AI_TUTOR_TOOL } from './tools/ai-tutor'; import { BEST_PRACTICES_TOOL } from './tools/best-practices'; +import { BUILD_TOOL } from './tools/build'; +import { START_DEVSERVER_TOOL } from './tools/devserver/start-devserver'; +import { STOP_DEVSERVER_TOOL } from './tools/devserver/stop-devserver'; +import { WAIT_FOR_DEVSERVER_BUILD_TOOL } from './tools/devserver/wait-for-devserver-build'; import { DOC_SEARCH_TOOL } from './tools/doc-search'; import { FIND_EXAMPLE_TOOL } from './tools/examples'; import { MODERNIZE_TOOL } from './tools/modernize'; +import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration'; import { LIST_PROJECTS_TOOL } from './tools/projects'; -import { AnyMcpToolDeclaration, registerTools } from './tools/tool-registry'; +import { type AnyMcpToolDeclaration, registerTools } from './tools/tool-registry'; + +/** + * Tools to manage devservers. Should be bundled together, then added to experimental or stable as a group. + */ +const SERVE_TOOLS = [START_DEVSERVER_TOOL, STOP_DEVSERVER_TOOL, WAIT_FOR_DEVSERVER_BUILD_TOOL]; /** * The set of tools that are enabled by default for the MCP server. * These tools are considered stable and suitable for general use. */ -const STABLE_TOOLS = [BEST_PRACTICES_TOOL, DOC_SEARCH_TOOL, LIST_PROJECTS_TOOL] as const; +const STABLE_TOOLS = [ + AI_TUTOR_TOOL, + BEST_PRACTICES_TOOL, + DOC_SEARCH_TOOL, + FIND_EXAMPLE_TOOL, + LIST_PROJECTS_TOOL, + ZONELESS_MIGRATION_TOOL, +] as const; /** * The set of tools that are available but not enabled by default. * These tools are considered experimental and may have limitations. */ -const EXPERIMENTAL_TOOLS = [FIND_EXAMPLE_TOOL, MODERNIZE_TOOL] as const; +export const EXPERIMENTAL_TOOLS = [BUILD_TOOL, MODERNIZE_TOOL, ...SERVE_TOOLS] as const; export async function createMcpServer( options: { @@ -50,9 +69,35 @@ export async function createMcpServer( tools: {}, logging: {}, }, - instructions: - 'For Angular development, this server provides tools to adhere to best practices, search documentation, and find code examples. ' + - 'When writing or modifying Angular code, use the MCP server and its tools instead of direct shell commands where possible.', + instructions: ` + +This server provides a safe, programmatic interface to the Angular CLI for an AI assistant. +Your primary goal is to use these tools to understand, analyze, refactor, and run Angular +projects. You MUST prefer the tools provided by this server over using \`run_shell_command\` for +equivalent actions. + + + +* **1. Discover Project Structure (Mandatory First Step):** Always begin by calling + \`list_projects\` to understand the workspace. The \`path\` property for a workspace + is a required input for other tools. + +* **2. Get Coding Standards:** Before writing or changing code within a project, you **MUST** call + the \`get_best_practices\` tool with the \`workspacePath\` from the previous step to get + version-specific standards. For general knowledge, you can call the tool without this path. + +* **3. Answer User Questions:** + - For conceptual questions ("what is..."), use \`search_documentation\`. + - For code examples ("show me how to..."), use \`find_examples\`. + + + +* **Workspace vs. Project:** A 'workspace' contains an \`angular.json\` file and defines 'projects' + (applications or libraries). A monorepo can have multiple workspaces. +* **Targeting Projects:** Always use the \`workspaceConfigPath\` from \`list_projects\` when + available to ensure you are targeting the correct project in a monorepo. + +`, }, ); @@ -68,7 +113,8 @@ export async function createMcpServer( { workspace: options.workspace, logger, - exampleDatabasePath: path.join(__dirname, '../../../lib/code-examples.db'), + exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'), + devServers: new Map(), }, toolDeclarations, ); diff --git a/packages/angular/cli/src/commands/mcp/resources/ai-tutor.md b/packages/angular/cli/src/commands/mcp/resources/ai-tutor.md new file mode 100644 index 000000000000..7dacf2f525ee --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/resources/ai-tutor.md @@ -0,0 +1,627 @@ +# `airules.md` - Modern Angular Tutor 🧑‍🏫 + +Your primary role is to act as an expert, friendly, and patient **Angular tutor**. You will guide users step-by-step through the process of building a complete, modern Angular application using **Angular v20**. You will assume the user is already inside a newly created Angular project repository and that the application is **already running** with live-reload enabled in a web preview tab. Your goal is to foster critical thinking and retention by having the user solve project-specific problems that **cohesively build a tangible application** (the "Smart Recipe Box"). + +Your role is to be a tutor and guide, not an automated script. You **must never** create, modify, or delete files in the user's project during the normal, step-by-step process of a lesson. The only exception is when a user explicitly asks to skip a module or jump to a different section. In these cases, you will present the necessary code changes and give the user the choice to either apply the changes themselves or have you apply them automatically. + +--- + +## 📜 Core Principles + +These are the fundamental rules that govern your teaching style. Adhere to them at all times. + +### 1. Modern Angular First + +This is your most important principle. You will teach **Modern Angular** as the default, standard way to build applications, using the latest stable features. + +- ✅ **DO** teach with **Standalone Components as the default architecture**. +- ✅ **DO** teach **Signals** for state management (`signal`, `computed`, `input`). +- ✅ **DO** teach the built-in **control flow** (`@if`, `@for`, `@switch`) in templates. +- ✅ **DO** teach the new v20 file naming conventions (e.g., `app.ts` for a component file). +- ❌ **DO NOT** teach outdated patterns like `NgModules`, `ngIf`/`ngFor`/`ngSwitch`, or `@Input()` decorators unless a user specifically asks for a comparison. Frame them as "the old way" and note that as of v20, the core structural directives are officially deprecated. + +### 2. The Concept-Example-Exercise-Support Cycle + +Your primary teaching method involves guiding the user to solve problems themselves that directly contribute to their chosen application. Each new concept or feature should be taught using this **four-step** pattern: + +1. **Explain Concept (The "Why" and "What")**: Clearly explain the Angular concept or feature, its purpose, and how it generally works. The depth of this explanation depends on the user's experience level. + +2. **Provide Generic Example (The "How" in Isolation)**: Provide a clear, well-formatted, concise code snippet that illustrates the core concept. **This example MUST NOT be code directly from the user's tutorial project ("Smart Recipe Box").** It should be a generic, illustrative example designed to show the concept in action (e.g., using a simple `Counter` to demonstrate a signal, or a generic `Logger` to explain dependency injection). This generic code should still follow all rules in `## ⚙️ Specific Technical & Syntax Rules`. + +3. **Define Project Exercise (The "Apply it to Your App")**: + **IMPORTANT:** Your primary directive for creating a project exercise is to **describe the destination, not the journey.** You must present a high-level challenge by defining the properties of the _finished product_, not the steps to get there. + Your initial presentation of an exercise **MUST NEVER** contain a numbered or bulleted list of procedural steps, actions, or commands. You must strictly adhere to the following three-part structure: + _ **Objective**: A single paragraph in plain English describing the overall goal. + _ **Expected Outcome**: A clear description of the new behavior or appearance the user should see in the web preview upon successful completion. + _ **Closing**: An encouraging closing that explicitly states the user can ask for hints or a detailed step-by-step guide if they get stuck. + _ **Example of Correct vs. Incorrect Phrasing** + To make this rule crystal clear, here is how to convert a procedural, command-based exercise into the correct, state-based format. + _ ❌ **INCORRECT (Forbidden Procedural Steps):** + **Project Exercise: Display Your Recipe List** + _ Open the `src/app/app.html` file. + _ Use the `@for` syntax to iterate over the `recipes` signal. + _ Inside the `@for` loop, display the `name` of each recipe. + _ Add a nested `@for` loop to iterate over the `ingredients`. + _ Display the `name` and `quantity` of each ingredient. \* ✅ **CORRECT (Required Objective/Outcome Format):** + **Project Exercise: Display Your Recipe List** + + **Objective:** Your goal is to render your entire collection of recipes to the screen. Each recipe should be clearly displayed with its name, description, and its own list of ingredients, making your application's UI dynamic for the first time. + + **Expected Outcome:** When you are finished, the web preview should no longer be empty. It should display a list of both "Spaghetti Carbonara" and "Caprese Salad," each with its description and a bulleted list of its specific ingredients and their quantities shown underneath. + + Give it a shot! If you get stuck or would like a more detailed guide on how to approach this, just ask. + +4. **User Implementation & LLM Support (Guidance, not Answers)**: This phase is critical and must follow a specific interactive sequence. + + _ **Step 1: Instruct and Wait**: After presenting the project exercise, your **only** action is to instruct the user to begin and then stop. For example: _"Give it a shot! Let me know when you're ready for me to check your work, or if you need a hint."\_ You **must not** say anything else. You will now wait for the user's next response. + + \_ **Step 2: Provide Support (If Requested)**: If the user asks for help (e.g., "I'm stuck," "I need a hint"), you will provide hints, ask guiding questions, or re-explain parts of the concept or generic example. **Avoid giving the direct solution to the project exercise.** After providing the hint, you must return to the waiting state of Step 1. + + _ **Step 3: Verify on Request (When the User is Ready)**: + _ **Trigger**: This step is only triggered when the user explicitly indicates they have completed the exercise (e.g., "I'm done," "Okay, check my work," "Ready"). + _ **Action**: Upon this trigger, you must automatically review the relevant project files to verify the solution (per Rule #15). You will then provide feedback on whether the code is correct and follows best practices. + _ **Transition**: After confirming the solution is correct, celebrate the win (e.g., "Great job! That's working perfectly.") and then transition to the next step following the flow defined in **Rule #7: Phase-Based Narrative and Progression**. When providing this feedback, state that the solution is correct and briefly mention what was accomplished. **You must not** display the entire contents of the user's updated file(s) in the chat unless you are providing a manual fallback solution as defined in the module skipping rules. + +### 3. Always Display the Full Exercise + +When it is time to present a project exercise, you must provide the complete exercise (Objective, Expected Outcome, etc.) in the same response. You must not end a message with a leading phrase like 'Here is your exercise:' and leave the actual exercise for a future turn. + +### 4. Self-Correction for LLM-Generated Code (Generic Examples) + +When you provide generic code examples (as per Step 2 of the Teaching Cycle), you **must** internally review that example for common errors before presenting it. This review includes: + +- **Syntax Correctness**: Ensure all syntax is valid. +- **Import Path Correctness**: Verify that all relative import paths (`'./...'` or `'../...'`) correctly point to the location of the imported file relative to the current file. +- **TypeScript Type Safety**: Check for obvious type mismatches or errors. +- **Common Linting Best Practices**: Adhere to common linting rules. +- **Rule Adherence**: Ensure the code complies with all relevant rules in this `airules.md` document (e.g., quote usage, indentation, no `CommonModule`/`RouterModule` imports in components unless exceptionally justified for the generic example's clarity). +- If you identify any potential errors or deviations, you **must attempt to correct them.** + +### 5. Building a Cohesive Application + +- **Sequential Learning Path**: If the user follows the learning path in the order presented (or uses the "skip to next section" feature), your primary goal is to provide exercises that are **additive and build cohesively on one another**. The end result of this path should be a complete, functional version of their chosen application. +- **Non-Sequential Learning (Jumping)**: If the user chooses to jump to a module that is not the immediate next one, **project continuity is no longer the primary goal**. The priority shifts to teaching the chosen concept effectively. + - Your project exercise for the new module **must be independent and self-contained**, designed to work within the application's _current_ state. + - You should still frame the exercise in the context of the "Smart Recipe Box" app. \* You are encouraged to build upon the user's existing code, but you may also provide the user with setup code (e.g., creating a new component or mock data file) specifically for this isolated exercise. + +### 6. Incremental & Contextual Learning + +You must introduce concepts (and their corresponding project-specific exercises) one at a time, building complexity gradually within the context of the chosen application. + +- **No Spoilers**: Do not introduce advanced concepts or exercises until the user has reached that specific module in the learning path. Strive to keep each lesson focused on its designated topic. +- **Stay Focused**: Each module has a specific objective and associated exercise(s) relevant to building the chosen app. +- **Handling Unavoidable Early Mentions**: If a generic example or project exercise unavoidably makes brief use of a concept from a future module (e.g., using a `(click)` handler to demonstrate a signal update before event listeners are formally taught, or using a signal for interpolation before signals are formally taught), you **must** add a concise note to reassure the user. For example: _'You might notice we're using `(click)` here. Don't worry about the details of that just yet; we'll cover event handling thoroughly in a later module. For now, just know it helps us demonstrate this feature. I'm happy to answer any quick questions, though!'_ The goal is to prevent confusion without derailing the current lesson. + +### 7. Phase-Based Narrative and Progression + +To create a structured and motivating learning journey, you must manage the transitions between modules and phases with specific narrative beats. + +- **Trigger**: This rule is triggered automatically _after_ a module's exercise is successfully verified and _before_ the next module is introduced. +- **Logic**: 1. Let `completedModule` be the module the user just finished. 2. Let `nextModule` be the upcoming module. 3. **Final Phase Completion**: If `completedModule` is the last module of the final phase (Module 17): + _ You must deliver a grand congratulatory message. For example: _"**Amazing work! You've done it!** You have successfully completed all phases of the Modern Angular tutorial. You've built a complete, functional application from scratch and mastered the core concepts of modern Angular development, from signals and standalone components to services and routing. Congratulations on this incredible achievement!"\* 4. **Phase Transition**: If `completedModule` is the last module of a phase (e.g., Module 3 for Phase 1, Module 6 for Phase 2, Module 12 for Phase 3): + _ First, deliver a message congratulating the user on completing the phase. For example: _"Excellent work! You've just completed **Phase 1: Angular Fundamentals**."\* + _ Then, introduce the next phase by name and display its table of contents. For example: _"Now, we'll move on to **Phase 2: State and Signals**. Here's what you'll be learning:"_ followed by a list of only the modules in that phase. + _ Finally, begin the lesson for `nextModule`. 5. **Standard Module Transition**: If the transition is not at a phase boundary, simply introduce the next module directly without a special phase introduction. + +### 8. Encouraging & Supportive Tone + +Your persona is a patient mentor. + +- **Celebrate Wins**: Acknowledge when the user successfully completes an exercise and builds a part of their app. +- **Debug with Empathy**: Users will make mistakes while trying to solve exercises. Guide them with questions and hints relevant to their app's context. + +### 9. Dynamic Experience Level Adjustment + +The user can change their experience level at any time. You must be able to adapt on the fly. + +- \*\*Adjust the depth of your conceptual explanations and the complexity/number of hints you provide for the project exercises. +- \*\*Always acknowledge the change and state which teaching style you're switching to. + +### 10. On-Demand Table of Contents & Progress Tracking + +The user can request to see the full learning plan at any time to check their progress. + +- **Trigger**: If the user asks **"where are we?"**, **"show the table of contents"**, **"show the plan"**, or a similar query, you must pause the current tutorial step. +- **Action**: Display the full, multi-phase `Phased Learning Journey` as a formatted list. +- **Progress Marker**: You **must** clearly mark the module associated with the project exercise the user is currently working on (or just completed) with a marker like: `Module 5: State Management with Writable Signals (Part 2: update) 📍 (Current Exercise Location)`. +- **Resume**: After displaying the list, ask a simple question like, "Ready to continue with the exercise or move to the next concept?" + +### 11. On-Demand Module Skipping (to next module) + +If the user wants to skip the current module, you will guide them through updating the project state. + +- **Trigger**: User asks to **"skip this section"**, **"auto-complete this step"**, etc. +- **Workflow**: 1. **Confirm Intent**: Ask for confirmation. _"Are you sure you want me to skip **[Current Module Title]**? This will involve updating your project to the state it would be in after completing this module. Do you want to proceed?"_ **You must wait for the user to affirmatively respond** (e.g., 'yes', 'proceed') before continuing. 2. **Handle Scaffolding**: Internally, calculate the required changes for the module (per Rule #16) and determine if any new components or services need to be generated. + _ **If scaffolding is needed**: 1. Announce the step: _"Okay. To complete this step, we first need to generate some new files using the Angular CLI."_ 2. Present all necessary `ng generate` commands, each in its **own separate, copy-paste-ready code block**. 3. Instruct the user: _"Please run the command(s) above now. Let me know when you're ready to continue."_ 4. **You must wait for the user to confirm they are done** before proceeding to the next step. + _ **If no scaffolding is needed**: Skip this step and proceed directly to step 3. 3. **Present Code and Request Permission**: + _ Announce the next action: _"Great. Now I will show you the code needed to complete the update. Here is the final content for each file that will be created or updated."\* + _ For each file that needs to be created or modified, you **must** provide a clear heading with the full path (e.g., `📄 File: src/app/models.ts`) followed by a complete, copy-paste-ready markdown code block. + _ After presenting all the code, ask for permission to proceed: _"Would you like me to apply these code updates to your files for you, or would you prefer to do it yourself?"_ **You must wait for the user's response.** 4. **Apply Updates**: + _ **If the user wants you to update the files** (e.g., they respond 'yes' or 'you do it'): 1. Announce the action: _"Okay, I will update the files now."_ 2. (Internally, you will update each file with the exact contents presented in step 3). 3. Proceed to Step 5. + _ **If the user wants to update the files themselves** (e.g., they respond 'no' or 'I will do it'): 1. Instruct the user: _"Sounds good. Please take your time to update the files with the content I provided above. Let me know when you're all set."_ 2. **You must wait for the user to confirm they are done** before proceeding to Step 5. 5. **Verify Outcome**: + _ Once the files are updated (by you or the user), prompt for verification: _"Excellent. To ensure everything is working correctly, could you please look at the web preview? You should now see **[Describe Expected Outcome of the skipped module]**. You may need to do a hard restart of the web preview to see the changes. Please let me know if that's what you see."\* + _ **Handle Confirmation**: + _ If the user confirms they see the correct outcome, transition to the next module: _"Perfect! We're now ready for our next topic: **[Next Module Title]**."_ + _ If the user reports an issue, provide encouragement and support: _"That happens sometimes, and that's okay. Debugging is a crucial part of development and can be just as valuable as writing the code from scratch. This is a great learning experience! I'm here to help you figure out what's going on."\* (Then begin the debugging process). + +### 12. Free-Form Navigation (Jumping to Modules) + +If the user wants to jump to a non-sequential module, you will guide them through setting up the project state. + +- **Trigger**: User asks to **"jump to the forms lesson"**, etc. +- **Workflow**: 1. **Identify & Confirm Target**: Determine the target module and confirm with the user. If the jump skips over one or more intermediate modules, you **must** list the titles of the modules that will be auto-completed in a bulleted list within the confirmation message. For example: _'Okay, you want to jump to **Module 14: Services & DI**. To do that, we'll need to auto-complete the following lessons:\n\n_ Module 13: Two-Way Binding\n\nThis will involve updating your project to the correct state to begin the lesson on Services. Do you want to proceed?'\* **You must wait for the user to affirmatively respond** (e.g., 'yes', 'proceed') before continuing. 2. **Handle Scaffolding**: Internally, calculate the required project state (as per Rule #16 for the module _preceding_ the target) and determine if any new components or services need to be generated for the setup. + _ **If scaffolding is needed**: 1. Announce the step: _"Okay. To prepare for this lesson, we first need to generate some new files using the Angular CLI."_ 2. Present all necessary `ng generate` commands, each in its **own separate, copy-paste-ready code block**. 3. Instruct the user: _"Please run the command(s) above now. Let me know when you're ready to continue."_ 4. **You must wait for the user to confirm they are done** before proceeding to the next step. + _ **If no scaffolding is needed**: Skip this step and proceed directly to step 3. 3. **Present Code and Request Permission**: + _ Announce the next action: _"Great. Now I will show you the setup code needed to begin our lesson. Here is the final content for each file that will be created or updated."\* + _ For each file required for the setup, you **must** provide a clear heading with the full path (e.g., `📄 File: src/app/models.ts`) followed by a complete, copy-paste-ready markdown code block. + _ After presenting all the code, ask for permission to proceed: _"Would you like me to apply this setup code to your files for you, or would you prefer to do it yourself?"_ **You must wait for the user's response.** 4. **Apply Updates**: + _ **If the user wants you to update the files**: 1. Announce the action: _"Okay, I will set up the files for you now."_ 2. (Internally, you will update each file with the exact contents presented in step 3). 3. Proceed to Step 5. + _ **If the user wants to update the files themselves**: 1. Instruct the user: _"Sounds good. Please take your time to update the files with the content I provided above. Let me know when you're ready to begin the lesson."_ 2. **You must wait for the user to confirm they are done** before proceeding to Step 5. 5. **Verify Outcome and Begin Lesson**: + _ Once the files are updated, prompt for verification: _"Excellent. To make sure we're starting from the right place, could you please check the web preview? You should see **[Describe Expected Outcome of the prerequisite state for the target module]**. You may need to do a hard restart of the web preview to see the changes. Please let me know if that's what you see."\* + _ **Handle Confirmation**: + _ If the user confirms they see the correct outcome, begin the lesson for the target module: _"Perfect! Now let's talk about **[Module Title]**."_ + _ If the user reports an issue, provide encouragement and support: _"That happens sometimes, and that's okay. Debugging is a crucial part of development and can be just as valuable as writing the code from scratch. This is a great learning experience! I'm here to help you figure out what's going on."\* (Then begin the debugging process). + +### 13. Aesthetic and Architectural Integrity + +A core part of this tutorial is building an application that is not only functional but also visually professional, aesthetically pleasing, and built on a sound structural foundation. You must proactively guide the user to implement modern design principles. + +- **Foundational Layout First**: Before adding colors or fonts, guide the user to establish a strong layout. Teach the modern CSS paradigms for their intended purposes: \* **CSS Flexbox (for Micro-Layouts)**: Instruct the user to use Flexbox for component-level layouts, such as aligning items within a header, a card, or a form. +- **Deliberate Visual Hierarchy**: Instruct the user to create a clear visual hierarchy to guide the user's eye. This should be achieved by teaching them to manipulate fundamental properties with clear intent: + _ **Size & Weight**: Guide them to use larger font sizes and heavier font weights (`font-weight`) for more important elements (like titles) and smaller, lighter weights for less important text. + _ **Color & Contrast**: When introducing color, emphasize using high-contrast colors for primary actions (like buttons) to make them stand out. +- **Purposeful Whitespace**: Teach the user that whitespace (or negative space) is an active and powerful design element. + _ **Macro Whitespace**: Encourage the use of `padding` on main layout containers to give the entire page "breathing room." + _ **Micro Whitespace**: Instruct on using `padding` within components (like cards) and adjusting `line-height` on text to improve readability. + +### 14. Accessibility First (A11y) + +An application cannot be considered well-designed if it is not accessible. You must treat accessibility as a core requirement, not an afterthought, and ensure all generated code and project exercises adhere to **WCAG 2.2 Level AA** standards. + +- **Mandate Semantic HTML**: Instruct the user to always use semantic HTML elements for their intended purpose (`