diff --git a/.github/codeql/config.yml b/.github/codeql/config.yml deleted file mode 100644 index ad81a268eda4..000000000000 --- a/.github/codeql/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: 'Angular CLI CodeQL config' - -query-filters: - # TODO(josephperrott): reevaluate if these can be reenabled. - - exclude: - id: js/bad-code-sanitization - - exclude: - id: js/regex-injection diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml index eb299f1e8855..7bcd8eaa270d 100644 --- a/.github/workflows/assistant-to-the-branch-manager.yml +++ b/.github/workflows/assistant-to-the-branch-manager.yml @@ -2,6 +2,7 @@ name: DevInfra on: push: + # zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code} pull_request_target: types: [opened, synchronize, reopened, ready_for_review, labeled] @@ -14,9 +15,9 @@ jobs: runs-on: ubuntu-latest if: github.event.repository.fork == false steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: angular/dev-infra/github-actions/branch-manager@ba726e7bca0b08b125ccc6f93c233749e1213c17 + - uses: angular/dev-infra/github-actions/branch-manager@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57b31cb9c2f1..8395ac196c14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Generate JSON schema types @@ -44,11 +44,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -61,11 +61,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -84,13 +84,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -100,11 +100,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Install node modules @@ -137,7 +137,7 @@ jobs: runs-on: windows-2025 steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Download built Windows E2E tests @@ -164,13 +164,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -188,13 +188,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} - name: Run CLI E2E tests @@ -208,34 +208,35 @@ jobs: SAUCE_TUNNEL_IDENTIFIER: angular-cli-${{ github.workflow }}-${{ github.run_number }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Start Sauce Connect + uses: saucelabs/sauce-connect-action@cb88b508c6f9ff4d84490093733315dbd55de022 # v3 + timeout-minutes: 10 + with: + username: ${{ vars.SAUCE_USERNAME }} + accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} + region: us-west-1 + tunnelName: ${{ env.SAUCE_TUNNEL_IDENTIFIER }} + proxyLocalhost: allow + # By default we disable SSL bumping for all requests. This is because SSL bumping is not needed + # for our test setup and in order to perform the SSL bumping, Saucelabs intercepts all HTTP + # requests in the tunnel VM and modifies them. This can cause flakiness as it makes all requests + # dependent on the SSL bumping middleware. + # See: https://wiki.saucelabs.com/display/DOCS/Troubleshooting+Sauce+Connect#TroubleshootingSauceConnect-DisablingSSLBumping + tlsPassthroughDomains: all - name: Run E2E Browser tests env: SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - SAUCE_LOG_FILE: /tmp/angular/sauce-connect.log - SAUCE_READY_FILE: /tmp/angular/sauce-connect-ready-file.lock - SAUCE_PID_FILE: /tmp/angular/sauce-connect-pid-file.lock - SAUCE_TUNNEL_IDENTIFIER: 'angular-${{ github.run_number }}' - SAUCE_READY_FILE_TIMEOUT: 120 - run: | - ./scripts/saucelabs/start-tunnel.sh & - ./scripts/saucelabs/wait-for-tunnel.sh - pnpm bazel test --config=saucelabs //tests:e2e.saucelabs - ./scripts/saucelabs/stop-tunnel.sh - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: ${{ failure() }} - with: - name: sauce-connect-log - path: ${{ env.SAUCE_CONNECT_DIR_IN_HOST }}/sauce-connect.log + run: pnpm bazel test --config=saucelabs //tests:e2e.saucelabs publish-snapshots: needs: build @@ -244,11 +245,11 @@ jobs: CIRCLE_BRANCH: ${{ github.ref_name }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - run: pnpm admin snapshots --verbose env: SNAPSHOT_BUILDS_GITHUB_TOKEN: ${{ secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index dd199499e759..000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: 'CodeQL' - -on: - push: - branches: ['main', '*.*.x'] - schedule: - - cron: '39 9 * * 1' - -permissions: {} - -jobs: - analyze: - name: Analyze - runs-on: 'ubuntu-latest' - permissions: - security-events: write - packages: read - strategy: - fail-fast: false - steps: - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Initialize CodeQL - uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 - with: - languages: javascript-typescript - build-mode: none - config-file: .github/codeql/config.yml - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 - with: - category: '/language:javascript-typescript' diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml index e0efdf2b94e4..3ff292ec3ff9 100644 --- a/.github/workflows/dev-infra.yml +++ b/.github/workflows/dev-infra.yml @@ -1,6 +1,7 @@ name: DevInfra on: + # zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code} pull_request_target: types: [opened, synchronize, reopened] issues: @@ -15,21 +16,21 @@ jobs: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/labeling/pull-request@649c3afeaa46674507b9625537e49de54a695e2b + - uses: angular/dev-infra/github-actions/labeling/pull-request@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} post_approval_changes: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/post-approval-changes@649c3afeaa46674507b9625537e49de54a695e2b + - uses: angular/dev-infra/github-actions/post-approval-changes@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} issue_labels: if: github.event_name == 'issues' runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/labeling/issue@649c3afeaa46674507b9625537e49de54a695e2b + - uses: angular/dev-infra/github-actions/labeling/issue@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main with: angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }} diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index dac67ebcbe7f..8b7edf35f34b 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -9,7 +9,6 @@ on: permissions: contents: 'read' - id-token: 'write' defaults: run: @@ -23,7 +22,7 @@ jobs: workflows: ${{ steps.workflows.outputs.workflows }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - id: workflows @@ -33,14 +32,17 @@ jobs: timeout-minutes: 30 runs-on: ubuntu-latest needs: list + permissions: + contents: 'read' + id-token: 'write' strategy: matrix: workflow: ${{ fromJSON(needs.list.outputs.workflows) }} steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile # We utilize the google-github-actions/auth action to allow us to get an active credential using workflow @@ -52,4 +54,7 @@ jobs: project_id: 'internal-200822' workload_identity_provider: 'projects/823469418460/locations/global/workloadIdentityPools/measurables-tracking/providers/angular' service_account: 'measures-uploader@internal-200822.iam.gserviceaccount.com' - - run: pnpm ng-dev perf workflows --name ${{ matrix.workflow }} --commit-sha ${{github.sha}} + - run: pnpm ng-dev perf workflows --name "$WORKFLOW" --commit-sha "$COMMIT_SHA" + env: + WORKFLOW: ${{ matrix.workflow }} + COMMIT_SHA: ${{ github.sha }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dd68d34f72e7..44071b0478ef 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,10 +20,10 @@ jobs: outputs: snapshots: ${{ steps.filter.outputs.snapshots }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 id: filter with: filters: | @@ -34,11 +34,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup ESLint Caching - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .eslintcache key: ${{ runner.os }}-${{ hashFiles('.eslintrc.json') }} @@ -66,17 +66,17 @@ jobs: # it has been merged. run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }} - name: Check Package Licenses - uses: angular/dev-infra/github-actions/linting/licenses@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/linting/licenses@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main build: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Build release targets @@ -93,11 +93,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Run module and package tests @@ -114,13 +114,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Run CLI E2E tests run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests:e2e.${{ matrix.subset }}_node${{ matrix.node }} @@ -128,11 +128,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Build E2E tests for Windows on Linux @@ -156,7 +156,7 @@ jobs: runs-on: windows-2025 steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Download built Windows E2E tests @@ -183,13 +183,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Run CLI E2E tests run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=3 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests:e2e.${{ matrix.subset }}_node${{ matrix.node }} @@ -205,12 +205,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Install node modules run: pnpm install --frozen-lockfile - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/setup@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@ba726e7bca0b08b125ccc6f93c233749e1213c17 + uses: angular/dev-infra/github-actions/bazel/configure-remote@6d918a6dd670e28afd4ba54d00b805d4aba43dca # main - name: Run CLI E2E tests run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 index 3cf9ef0b1a8d..000000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: OpenSSF Scorecard -on: - branch_protection_rule: - schedule: - - cron: '0 2 * * 0' - push: - branches: [main] - workflow_dispatch: - -# Declare default permissions as read only. -permissions: - contents: read - -jobs: - analysis: - name: Scorecards analysis - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Needed to publish results - id-token: write - - steps: - - name: 'Checkout code' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: 'Run analysis' - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - # Upload the results as artifacts. - - name: 'Upload artifact' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard. - - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 - with: - sarif_file: results.sarif diff --git a/.nvmrc b/.nvmrc index 941d7c071de8..60ade1ae01e8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.22.3 +24.19.0 diff --git a/BUILD.bazel b/BUILD.bazel index 15200e8e228f..959b28996ec7 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -106,6 +106,6 @@ config_setting( ) validate_ts_version_matching( - module_lock_file = "MODULE.bazel.lock", + module_bazel = "MODULE.bazel", package_json = "package.json", ) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3b18b1101e..f3077ffb76bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,777 @@ + + +# 22.2.0-next.4 (2026-08-19) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [34e1e0bb5](https://github.com/angular/angular-cli/commit/34e1e0bb5d5ccf374f51a1b241158f6f2ea0dd42) | fix | enforce MCP roots in get_best_practices tool | +| [2b060630c](https://github.com/angular/angular-cli/commit/2b060630c66ce688b7ecbe51d282955869e5d4b0) | fix | handle errors from isAllowedWorkspacePath in best-practices tool | +| [24fd8fce2](https://github.com/angular/angular-cli/commit/24fd8fce2a3ff035acdc5f3e036eb4ba21f71a76) | fix | throw on out-of-roots workspace in best-practices tool | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [ce1b60f89](https://github.com/angular/angular-cli/commit/ce1b60f89699c3a76496a0489e5f2b9e4fe62429) | fix | transform fail() to expect.fail() in refactor-jasmine-vitest | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------- | +| [175273931](https://github.com/angular/angular-cli/commit/175273931ebb0cf08bf62dd1694dbde5ff5229b6) | feat | Support splitting browser and server stats jsonfiles for easier consumption | +| [1ee0beca7](https://github.com/angular/angular-cli/commit/1ee0beca7b243cc4c06eb42b25d216d7b256b097) | fix | correct misleading error message for top-level await | +| [0ffe2d27f](https://github.com/angular/angular-cli/commit/0ffe2d27f256a5755809fa4788e4adb97c65b806) | fix | disable code splitting for unit test builds | +| [50994d76e](https://github.com/angular/angular-cli/commit/50994d76e59a4b632f1782c51cd60f80c9cd50fb) | fix | preserve integrity and crossorigin in autoCsp loader | +| [d6fd24320](https://github.com/angular/angular-cli/commit/d6fd243207ad1c3e242b8592698afa986320cc6c) | perf | traverse AST with iterative post-order walker in i18n inliner | + + + + + +# 22.1.5 (2026-08-19) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [e672271f8](https://github.com/angular/angular-cli/commit/e672271f81e7b8b422b98c17fca5b67896d40844) | fix | enforce MCP roots in get_best_practices tool | +| [a14916cc4](https://github.com/angular/angular-cli/commit/a14916cc4b13412205d3e4a1c32d7e40be52f0e1) | fix | handle errors from isAllowedWorkspacePath in best-practices tool | +| [d6e1cddff](https://github.com/angular/angular-cli/commit/d6e1cddff92842f6889585ae7899efc028decc78) | fix | throw on out-of-roots workspace in best-practices tool | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [ba2b0e4c2](https://github.com/angular/angular-cli/commit/ba2b0e4c291842c699fdbeaaddec1531dbdd7952) | fix | transform fail() to expect.fail() in refactor-jasmine-vitest | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [cba72902d](https://github.com/angular/angular-cli/commit/cba72902d42022bd5a1bd17b23d65cacc0013e35) | fix | correct misleading error message for top-level await | +| [b4679998c](https://github.com/angular/angular-cli/commit/b4679998c205b6cce01d40fad24c4ee7954aa48d) | fix | disable code splitting for unit test builds | +| [ff1d3565e](https://github.com/angular/angular-cli/commit/ff1d3565e87749a8347c3be98cd4a7ebccb69fc0) | fix | preserve integrity and crossorigin in autoCsp loader | +| [1fc1fb05c](https://github.com/angular/angular-cli/commit/1fc1fb05cfe2c69ee7b9345b582d3c2177b923fd) | perf | traverse AST with iterative post-order walker in i18n inliner | + + + + + +# 22.2.0-next.3 (2026-08-13) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- | +| [7106676e6](https://github.com/angular/angular-cli/commit/7106676e64ee234641b1b3cbd88fc058703d4d3e) | fix | disable searching current directory for bare executable names on Windows | +| [ecf8c0822](https://github.com/angular/angular-cli/commit/ecf8c0822e5d56b44a0be4be8fccb41373135d3e) | fix | serialize configuration as a single argv token in run_target strategies ([#33657](https://github.com/angular/angular-cli/pull/33657)) | +| [2e4dd90c3](https://github.com/angular/angular-cli/commit/2e4dd90c3ae898bf36af42f699c7b7d560bfc66a) | perf | avoid eager module loading during global bootstrap | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [1161e6c99](https://github.com/angular/angular-cli/commit/1161e6c99992c884e2e0ce9fc295b0e60b2ae1df) | fix | generate CLAUDE.md for Claude Code instead of AGENTS.md | +| [c536ae364](https://github.com/angular/angular-cli/commit/c536ae364975dd0088fd8717464c723a4dcb74e3) | fix | import UrlSegment instead of subPath in guard generator | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [04b532772](https://github.com/angular/angular-cli/commit/04b532772f6347d341b182135dee1758616a69d6) | fix | count statically imported chunks in the initial total | +| [2274babb6](https://github.com/angular/angular-cli/commit/2274babb662b8da2189f1ae9faaa528838052c32) | fix | normalize setupFiles paths to POSIX for vitest runner | +| [199a864df](https://github.com/angular/angular-cli/commit/199a864df97fc8c20b8d4e1fc8b891c61140671a) | fix | prevent syntax corruption for Crockford-style enum IIFE | +| [0d9851600](https://github.com/angular/angular-cli/commit/0d9851600ca618ccabfc3143a9c552a5448d6eec) | fix | recursively ignore output and cache paths in watch mode | +| [3eba6f726](https://github.com/angular/angular-cli/commit/3eba6f7269f3b0f8556510d0b668e978ea686c88) | fix | return direct file contents for non-Angular TypeScript files | +| [3c7ac1518](https://github.com/angular/angular-cli/commit/3c7ac1518b062f14409b4d3c6c3e65c956424ad9) | fix | return only lowest version per target engine | +| [3e9fed9a6](https://github.com/angular/angular-cli/commit/3e9fed9a6a1d24f30812ca29ca621d9d9a14496f) | fix | set target for Rolldown dependency prebundling in Vite dev server | +| [87551ad5c](https://github.com/angular/angular-cli/commit/87551ad5c53f90cbdc68fdb5e47007b9436c7f75) | perf | avoid encoding intermediate source maps before remapping | +| [194be2088](https://github.com/angular/angular-cli/commit/194be2088587918ed7b47ed606ea587a7116608a) | perf | avoid encoding the inline source map before remapping | +| [a55a6b78e](https://github.com/angular/angular-cli/commit/a55a6b78e89debb9c93c83375c54f6716bc26c65) | perf | batch last_accessed updates in sqlite cache store | +| [33b305416](https://github.com/angular/angular-cli/commit/33b3054169929dc98f93a758a90da5d26cf522f7) | perf | hash the i18n inline cache key options once per locale | +| [1c00edce0](https://github.com/angular/angular-cli/commit/1c00edce02bdd0201546417d71518f78bfb89d34) | perf | optimize sourcemap stripping and loading with buffer fast path | +| [b6269a816](https://github.com/angular/angular-cli/commit/b6269a8169046a78281808dcf92054140912a77d) | perf | optimize template string size calculation in server manifest | +| [a6ef9cfbe](https://github.com/angular/angular-cli/commit/a6ef9cfbeace725d58c0f7f65640ef6de9b39c33) | perf | replace watchpack with @parcel/watcher and chokidar | +| [596847f89](https://github.com/angular/angular-cli/commit/596847f89913684a84465d004aa3d6c7e1a40b06) | perf | share i18n translations with the inliner workers by reference | +| [19c91e48d](https://github.com/angular/angular-cli/commit/19c91e48d4b8a95a76dfe4bffd105130b416843d) | perf | use Map for chunk asset size lookups in budget calculator | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [6a3446770](https://github.com/angular/angular-cli/commit/6a3446770a8fbfecc7265eb2a6d764e2f1614b3a) | fix | destroy platform when response stream is cancelled | + + + + + +# 22.1.4 (2026-08-13) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- | +| [67a29899b](https://github.com/angular/angular-cli/commit/67a29899b07f760bac9582cbc6175966cce1c5d2) | fix | disable searching current directory for bare executable names on Windows | +| [1b0ba5c17](https://github.com/angular/angular-cli/commit/1b0ba5c17f0be2679b31bad1bee8b435315dac1a) | fix | serialize configuration as a single argv token in run_target strategies ([#33657](https://github.com/angular/angular-cli/pull/33657)) | +| [9440432d2](https://github.com/angular/angular-cli/commit/9440432d273c044965508d80e808c2fe123af0aa) | perf | avoid eager module loading during global bootstrap | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [d4a48e3ab](https://github.com/angular/angular-cli/commit/d4a48e3abe320e090eefe17cb70abdf8e7eafaed) | fix | generate CLAUDE.md for Claude Code instead of AGENTS.md | +| [d451f15b2](https://github.com/angular/angular-cli/commit/d451f15b2838a8282363dcb30161dd5dc858750f) | fix | import UrlSegment instead of subPath in guard generator | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [46fcb29d6](https://github.com/angular/angular-cli/commit/46fcb29d6c1ded962de95449d63716c13950f59a) | fix | count statically imported chunks in the initial total | +| [796b57684](https://github.com/angular/angular-cli/commit/796b576843191a4d5c74e4f9363ec0e46ae3a881) | fix | normalize setupFiles paths to POSIX for vitest runner | +| [c805e5cfb](https://github.com/angular/angular-cli/commit/c805e5cfb308d02a1944ef446d0a01af8a7ec6d2) | fix | prevent syntax corruption for Crockford-style enum IIFE | +| [cc6c17716](https://github.com/angular/angular-cli/commit/cc6c17716df5c411efca5e6f4c960f0565f45d90) | fix | prevent syntax corruption in oxc transform | +| [a142f6f83](https://github.com/angular/angular-cli/commit/a142f6f83d2f501b64f2404909ef9325bbfa70fb) | fix | return direct file contents for non-Angular TypeScript files | +| [956c6578d](https://github.com/angular/angular-cli/commit/956c6578d31d8482c04dc45723d7ab3b4fe355a5) | fix | return only lowest version per target engine | +| [689b5b116](https://github.com/angular/angular-cli/commit/689b5b116717140da98494e61ac5ea5ad7b68d73) | fix | set target for Rolldown dependency prebundling in Vite dev server | +| [70461f6ed](https://github.com/angular/angular-cli/commit/70461f6ed2e30aeaad2b93400ad00d9274665967) | perf | avoid encoding the inline source map before remapping | +| [22cce7563](https://github.com/angular/angular-cli/commit/22cce7563e82f17373b69e590d3e445767bd9264) | perf | batch last_accessed updates in sqlite cache store | +| [ab2ed18d5](https://github.com/angular/angular-cli/commit/ab2ed18d5739e8565403062ca74aa2d5cb3b95ec) | perf | hash the i18n inline cache key options once per locale | +| [34516ff73](https://github.com/angular/angular-cli/commit/34516ff736185e20ef8239b1fed8eda1f0193f62) | perf | optimize template string size calculation in server manifest | +| [712971c37](https://github.com/angular/angular-cli/commit/712971c370738beaebff18f6995859b1713ed8ae) | perf | share i18n translations with the inliner workers by reference | +| [78f8c5fef](https://github.com/angular/angular-cli/commit/78f8c5fef44dc6ff466b17c98fa1d01222cb1781) | perf | use Map for chunk asset size lookups in budget calculator | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [2331c1047](https://github.com/angular/angular-cli/commit/2331c10476830e7a53e0f2fa4a2f37dad58e2d5d) | fix | destroy platform when response stream is cancelled | + + + + + +# 21.2.21 (2026-08-12) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [ba5d16cac](https://github.com/angular/angular-cli/commit/ba5d16cacab4139897d1f0cf4161562b1bd00553) | fix | update dependency @modelcontextprotocol/sdk to v1.30.0 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [f7f60e69e](https://github.com/angular/angular-cli/commit/f7f60e69e77cda60378543a9ca9cc2c76c0bf34f) | fix | bump undici to 7.29.0 | + + + + + +# 20.3.34 (2026-08-12) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [3962517b9](https://github.com/angular/angular-cli/commit/3962517b95c76016ff7bcb53662704dff18c0552) | fix | update dependency @modelcontextprotocol/sdk to v1.30.0 | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [f348efe8b](https://github.com/angular/angular-cli/commit/f348efe8b6d9c9d7014afb331eecda0a9478ba0a) | fix | bump undici to 7.29.0 | + + + + + +# 22.2.0-next.2 (2026-08-05) + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [7e5510e464](https://github.com/angular/angular-cli/commit/7e5510e464eb423d8757ae6f058f73fc17249d81) | fix | disable linker internal source mapping in OXC linker | +| [bad31421cb](https://github.com/angular/angular-cli/commit/bad31421cbf23339d4fbd389a91d198df405697d) | fix | prevent syntax corruption in linker and oxc transform | + + + + + +# 22.2.0-next.1 (2026-08-05) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [41555dfb3b](https://github.com/angular/angular-cli/commit/41555dfb3b71d08cdfe2853bf2cbeca5b6942f67) | feat | add `--root` command line option to `mcp` command | +| [035b9d5c9b](https://github.com/angular/angular-cli/commit/035b9d5c9bca624b98e14480f599481f294b49f3) | fix | add packages with built-in schematics to devDependencies only | +| [0ae5690c35](https://github.com/angular/angular-cli/commit/0ae5690c35b9af00f60d4851f1bab4ba5ec8eb19) | fix | always install package during ng add to inspect manifest on disk for schematics ([#33718](https://github.com/angular/angular-cli/pull/33718)) | +| [44ff584b33](https://github.com/angular/angular-cli/commit/44ff584b33830aa839c08feb205f014e4e7b5dca) | fix | correctly report available package updates during bare ng update | +| [1d4f9ebbe9](https://github.com/angular/angular-cli/commit/1d4f9ebbe993a5eb8b23929a3fc55b506597e5aa) | fix | discover migrations from installed packages when omitted by registry metadata ([#33718](https://github.com/angular/angular-cli/pull/33718)) | +| [25034f97ad](https://github.com/angular/angular-cli/commit/25034f97ad1b9a12f4080f646a0e2705425f9726) | fix | normalize Windows drive-letter casing for process.cwd | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- | +| [ad8271320f](https://github.com/angular/angular-cli/commit/ad8271320f67c2b1502e446f1e3cb0a05444d04f) | feat | migrate Angular Linker to oxc-parser and magic-string | +| [4fb12292c2](https://github.com/angular/angular-cli/commit/4fb12292c21498c59836f7c80e4f824bb7751cac) | fix | prevent build process hang on unhandled error ([#33715](https://github.com/angular/angular-cli/pull/33715)) | +| [abd8ec9f3e](https://github.com/angular/angular-cli/commit/abd8ec9f3e11f1410fcb87d0d9d60f56016912fe) | fix | prevent IIFE wrapper interleaving for adjacent classes in minified files | +| [bcadbbef20](https://github.com/angular/angular-cli/commit/bcadbbef2054e897b9d22c1456883fc68f0383c0) | fix | retain watch files on error in load result cache | +| [e1c7193dce](https://github.com/angular/angular-cli/commit/e1c7193dcee1c7f2feb07fdf24a6e0656f29cfaf) | fix | support case-insensitive and alternative license file names | +| [f4a0f846e7](https://github.com/angular/angular-cli/commit/f4a0f846e7d14b5ff58c91d04461ef4a027c56b8) | perf | release build resources early in non-watch mode ([#33715](https://github.com/angular/angular-cli/pull/33715)) | + +### @angular/ssr + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [b74eef889f](https://github.com/angular/angular-cli/commit/b74eef889f9c8812dcbe116400663fa3d68d6b4f) | fix | settle writeResponseToNodeResponse when client disconnects | +| [33169801c6](https://github.com/angular/angular-cli/commit/33169801c6ed7fc7c7dca2c04008e5d405374983) | perf | avoid buffering request body when sanitizing headers | + + + + + +# 22.1.3 (2026-08-05) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [f837b788c6](https://github.com/angular/angular-cli/commit/f837b788c6a23e7661db5455192f5a84d20e9b37) | fix | always install package during ng add to inspect manifest on disk for schematics ([#33718](https://github.com/angular/angular-cli/pull/33718)) | +| [72ae93638d](https://github.com/angular/angular-cli/commit/72ae93638dd83b3877773ac0d9f7e4c4e9056a33) | fix | correctly report available package updates during bare ng update | +| [a949e06568](https://github.com/angular/angular-cli/commit/a949e0656855786e4274159be09242ff9c4eb012) | fix | discover migrations from installed packages when omitted by registry metadata ([#33718](https://github.com/angular/angular-cli/pull/33718)) | +| [1d9122fd50](https://github.com/angular/angular-cli/commit/1d9122fd50f8a9b82199d44f9b0e075726279538) | fix | normalize Windows drive-letter casing for process.cwd | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- | +| [a4860d505a](https://github.com/angular/angular-cli/commit/a4860d505aff0b43fc608cd0745ed187b1d407b5) | fix | prevent build process hang on unhandled error ([#33715](https://github.com/angular/angular-cli/pull/33715)) | +| [7686d66cab](https://github.com/angular/angular-cli/commit/7686d66cabcdfe90748ac405686d42674a19576f) | fix | retain watch files on error in load result cache | +| [25b7cf7504](https://github.com/angular/angular-cli/commit/25b7cf7504838d541abfba043d9ea3d616a23c04) | fix | support case-insensitive and alternative license file names | +| [4fd81402ce](https://github.com/angular/angular-cli/commit/4fd81402ce5f2f3db45a17ee0d116f12edf8d254) | perf | release build resources early in non-watch mode ([#33715](https://github.com/angular/angular-cli/pull/33715)) | + +### @angular/ssr + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [6af906cb94](https://github.com/angular/angular-cli/commit/6af906cb94bd0686b4c0431989e054711c74892a) | fix | settle writeResponseToNodeResponse when client disconnects | +| [917d3b2254](https://github.com/angular/angular-cli/commit/917d3b22549c19b91e03ceb0775a3a758142b9de) | perf | avoid buffering request body when sanitizing headers | + + + + + +# 21.2.20 (2026-08-05) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [972f6db7e9](https://github.com/angular/angular-cli/commit/972f6db7e9ccf3c4b0e895341a49b104a8b0ac9b) | fix | upgrade postcss to 8.5.23 | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [8de75ada50](https://github.com/angular/angular-cli/commit/8de75ada503d4182af7f0be957e58fe678a63b98) | fix | upgrade postcss to 8.5.23 | + + + + + +# 20.3.33 (2026-08-05) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [0b4008f17d](https://github.com/angular/angular-cli/commit/0b4008f17d76c821d9f075b86da80b826f085d23) | fix | upgrade postcss to 8.5.23 | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [fc861affcd](https://github.com/angular/angular-cli/commit/fc861affcd8866b71b81c8ba90f7a63890b6577c) | fix | upgrade postcss to 8.5.23 | + + + + + +# 22.1.2 (2026-07-30) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [57db27ea91](https://github.com/angular/angular-cli/commit/57db27ea910b102854267dfcd77e8fe60f23993c) | fix | add packages with built-in schematics to devDependencies only | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ | +| [baed978885](https://github.com/angular/angular-cli/commit/baed978885585fceadc64f0151d7474054d57d6a) | fix | prevent IIFE wrapper interleaving for adjacent classes in minified files | + + + + + +# 22.1.1 (2026-07-30) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [ab971a8543](https://github.com/angular/angular-cli/commit/ab971a8543d80dfc93fdddf49598564f25196160) | fix | support npm 12 metadata array and error formats | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [db24220fb8](https://github.com/angular/angular-cli/commit/db24220fb8efb5c9416e375a468672ff69d0659d) | fix | rewrite paths from sandboxed execroots when running under Bazel | + + + + + +# 22.2.0-next.0 (2026-07-30) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [e148a8df93](https://github.com/angular/angular-cli/commit/e148a8df93b309dcd51698b4ce4f9343560edd8e) | fix | support npm 12 metadata array and error formats | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [6469b5f1f5](https://github.com/angular/angular-cli/commit/6469b5f1f5946415f49000f26bddd88b5647ac3d) | fix | rewrite paths from sandboxed execroots when running under Bazel | + + + + + +# 22.1.0 (2026-07-29) + +## Deprecations + +### @angular-devkit/core + +- `stringToFileBuffer` and `fileBufferToString` are deprecated. Use standard Web APIs (`TextEncoder` and `TextDecoder`) instead. + + Internal usages within the repository have been removed and replaced with standard Web APIs. The public API golden file for `@angular-devkit/core` has been updated to reflect the deprecations. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [89d7f59cd](https://github.com/angular/angular-cli/commit/89d7f59cd7cc5d821ac0e81b1fee50e27877c976) | feat | update ai-config to include Angular MCP server config | +| [b345bb54d](https://github.com/angular/angular-cli/commit/b345bb54d3ec7c73f9db70400f823d90aeab8fa8) | fix | remove unused spec tsconfig outDir | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [f59bcf529](https://github.com/angular/angular-cli/commit/f59bcf52917bc5e25b97c863f96127ec8d0f12f5) | fix | abort and guide manual mitigation when updating catalog packages | +| [48f90f5d5](https://github.com/angular/angular-cli/commit/48f90f5d5e5dd6369b2ccf044a722271c82dffd6) | fix | prevent Yarn registry environment variable override | +| [c963ecbec](https://github.com/angular/angular-cli/commit/c963ecbec241d42dfb419ae081ba22429e37f09e) | fix | remove algoliasearch dependency and support latest docs versions | +| [6859d25c3](https://github.com/angular/angular-cli/commit/6859d25c32b6c93cbbf2d4ad4143aa10b52b7588) | fix | resolve correct registry name when using npm alias syntax during update | +| [3a2589dac](https://github.com/angular/angular-cli/commit/3a2589dac684dfe26b2151b940bd40280d7c5d8e) | fix | respect client-side release age settings during update resolution | +| [5c3101bff](https://github.com/angular/angular-cli/commit/5c3101bff2e1b9d0b0e4668a183e8bcab8a1540e) | fix | revert package.json changes if installation tasks fail during update | +| [7932caaf9](https://github.com/angular/angular-cli/commit/7932caaf987c5692d6624f6af23e65ce3f6d27fd) | fix | robustly parse npm manifest from array | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------- | +| [fd336d365](https://github.com/angular/angular-cli/commit/fd336d365dbfe8f558db177a8da24790914a541b) | refactor | deprecate stringToFileBuffer and fileBufferToString | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------- | +| [34d558c3c](https://github.com/angular/angular-cli/commit/34d558c3c98e1b59a2c2808fc04bfbb63dfe8acf) | feat | add built-in SQLite cache store fallback | +| [0c18dc0f6](https://github.com/angular/angular-cli/commit/0c18dc0f6d4f36d7bbb6f35b37bfa688e8fcf7d2) | feat | emit debug ids for stable subresource integrity hashes | +| [51f69276f](https://github.com/angular/angular-cli/commit/51f69276fd2b2aab0e94901e79a51f6860ef8690) | feat | enable chunk optimization for server builds | +| [10dc30f9c](https://github.com/angular/angular-cli/commit/10dc30f9c680f46f65b5beb030f2a75c422a3e71) | feat | migrate advanced optimization Babel plugins to oxc-parser + magic-string | +| [917393a4c](https://github.com/angular/angular-cli/commit/917393a4cd85172574b155f727da5c1eae196fb1) | feat | migrate i18n inliner to oxc-parser + magic-string | +| [52ae7f862](https://github.com/angular/angular-cli/commit/52ae7f862a581cac97d5a9b33c9b71b830d95b19) | feat | share persistent build cache across git worktrees | +| [95510511d](https://github.com/angular/angular-cli/commit/95510511d963384c5a03298300246aa34a62328a) | fix | add bounded timeout to vitest executor disposal | +| [62b11e316](https://github.com/angular/angular-cli/commit/62b11e316e8248161877592acee5954b2a8c9c66) | fix | anchor debug ID comment matching and make injection idempotent | +| [6d9b360e5](https://github.com/angular/angular-cli/commit/6d9b360e5f2b7ab3d4ace4db35f674166630f658) | fix | prevent esbuild service hang on internal component stylesheet builds | +| [291caab61](https://github.com/angular/angular-cli/commit/291caab6158fa804f6221dd024a5b96e6181eaad) | fix | prevent stripping nested sourceMappingURL comments | +| [0e3ca3dfb](https://github.com/angular/angular-cli/commit/0e3ca3dfba8ef60217089d93a321660ebb6fb838) | fix | support hyphens and underscores when stripping chunk hash in chunk optimizer | +| [585d08af8](https://github.com/angular/angular-cli/commit/585d08af881a35653dc5e7b1ee7a75b42e154517) | perf | default chunk optimization to use Rolldown | +| [09d0a11a8](https://github.com/angular/angular-cli/commit/09d0a11a8b832f156aea100ad3497c5221fc0f50) | perf | enable fast-path AST printing with sourcemaps in AotCompilation | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [7ef9ed24d](https://github.com/angular/angular-cli/commit/7ef9ed24d3c111474ae21c06cb2b05a21804f586) | feat | support the standard Forwarded header | + + + + + +# 22.0.9 (2026-07-29) + +### @angular/cli + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [9ee8f9b669](https://github.com/angular/angular-cli/commit/9ee8f9b6692b8efa2d0cabd502cd5a598f436c8a) | fix | declare devserver_start and devserver_stop as isReadOnly:false | + +### @angular/build + +| Commit | Type | Description | +| ---------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [d0c776d461](https://github.com/angular/angular-cli/commit/d0c776d4610e2327c29900b9b3d679e9c0f28061) | fix | safeguard Karma builder stream controller against closed state | + + + + + +# 22.0.8 (2026-07-23) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [41c356256](https://github.com/angular/angular-cli/commit/41c356256525469a41ea72896b1ddff91a4e6e55) | fix | batch Prettier invocations during migration formatting | +| [bc87c85c3](https://github.com/angular/angular-cli/commit/bc87c85c3c660e969cf77a493dc4ee58fd831214) | fix | update MCP devserver tool names to conform to spec regex | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [5724bf632](https://github.com/angular/angular-cli/commit/5724bf632eba59eccdd941beae8aafad35ce003e) | fix | canonicalize drive letter casing for workspace root on Windows | +| [b8bdbaa1e](https://github.com/angular/angular-cli/commit/b8bdbaa1ea1c58f061580bd58c015078f57c1530) | fix | ensure import map integrity keys are valid URL-like specifiers | +| [212a7bb21](https://github.com/angular/angular-cli/commit/212a7bb21eca131f32fd7b4e7b5a9ab87f2bfded) | fix | favor istanbul coverage provider when browser testing is enabled | +| [bc760b005](https://github.com/angular/angular-cli/commit/bc760b005e0dae10a2dfd31a7f0f772482305398) | fix | preserve custom config options in runnerConfig for vitest | +| [19cda7415](https://github.com/angular/angular-cli/commit/19cda7415693f36e911a736a32576110905b6600) | fix | remap metafile paths when workspace root is a symlink or junction | +| [fdcc98c79](https://github.com/angular/angular-cli/commit/fdcc98c7912f45431268fdf66c8b132bd119bc27) | perf | skip semantic affected-file walk when type checking is disabled | + + + + + +# 22.0.7 (2026-07-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [e85c10ccb](https://github.com/angular/angular-cli/commit/e85c10ccb551eeaf9ac7e819ee6eb095317b5032) | fix | copy packageManager field and yarn config for temp installs | +| [b87cafec0](https://github.com/angular/angular-cli/commit/b87cafec04b24c2f4337ecfa00388197ab5003f0) | fix | support resolving subproject dependencies in pnpm workspaces | + + + + + +# 21.2.19 (2026-07-09) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [df5429871](https://github.com/angular/angular-cli/commit/df54298712bb84ec079787e4ca6c0164f6358cf1) | fix | update http-proxy-middleware to 3.0.7 | + + + + + +# 20.3.32 (2026-07-09) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [d4e07cb3e](https://github.com/angular/angular-cli/commit/d4e07cb3e3f871822f9bf37fdea35814ec1cdb80) | fix | update http-proxy-middleware to 3.0.7 | + + + + + +# 22.0.6 (2026-07-08) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ | +| [df49966c6](https://github.com/angular/angular-cli/commit/df49966c6b8ec0c10b58fee18981fc2992ac071a) | fix | fallback to build target preserveSymlinks option in karma runner | +| [9906add5b](https://github.com/angular/angular-cli/commit/9906add5b722cca7f61488b367dd73545c6e13bc) | fix | prevent externalizing builder-injected i18n locale data | +| [070155f6e](https://github.com/angular/angular-cli/commit/070155f6ead185d4b478d9034662d65b8bb86a23) | fix | strip all vite id prefixes from minified code with external dependencies | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [c9aa354b1](https://github.com/angular/angular-cli/commit/c9aa354b1de0c946ddc994191613bb809a799f81) | fix | replace all route parameters when resolving relative redirects | + + + + + +# 22.0.5 (2026-07-01) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [f099e641c](https://github.com/angular/angular-cli/commit/f099e641c96a5932d81535e712af7044269b4788) | fix | gracefully handle package manager errors in command handler | +| [cf033ec48](https://github.com/angular/angular-cli/commit/cf033ec48e2371655fbf6bcdb588c05aff2e6662) | fix | respect release age policy in update bootstrapping logic | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- | +| [862a38069](https://github.com/angular/angular-cli/commit/862a380697db60cee8cf83e8fe20641db2a6aae1) | fix | remove unused app tsconfig outDir | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [f75085f4b](https://github.com/angular/angular-cli/commit/f75085f4b58ed61979a2901a4ef0353e56f22136) | fix | prevent OS command injection in ssr-dev-server builder | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [dfee1e7b1](https://github.com/angular/angular-cli/commit/dfee1e7b142d767c86eecb7791ed72bce65866c5) | fix | auto-inject localize/init in library unit tests | +| [aba718403](https://github.com/angular/angular-cli/commit/aba718403df7a96310570c1449674a49254cd4af) | fix | inherit preserveSymlinks option from build target in unit-test builder | +| [7c0d9f03c](https://github.com/angular/angular-cli/commit/7c0d9f03c2ae82a90a48e51b2aee731a36dc2beb) | fix | load zone.js dynamically for library unit tests | +| [4288e4452](https://github.com/angular/angular-cli/commit/4288e44525e855f29f026276f518968b1b2697db) | fix | scope createRequire module resolution using paths to prevent parent paths | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [5f774f891](https://github.com/angular/angular-cli/commit/5f774f891a086bf0a2ab6f86c24485425e99f16d) | fix | preserve response headers during redirect | + + + + + +# 21.2.18 (2026-07-01) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [61a810ab6](https://github.com/angular/angular-cli/commit/61a810ab615d1a26fc69f9e48be7cc6555a9c430) | fix | respect release age policy in update bootstrapping logic | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [a2b6116ed](https://github.com/angular/angular-cli/commit/a2b6116eddc7a71e4539817da623221ca552a35f) | fix | bump undici to 7.28.0 | +| [2510ee3c9](https://github.com/angular/angular-cli/commit/2510ee3c9aefdab5428938e78b9cfcea0ada1c45) | fix | bump vite to 7.3.6 | + + + + + +# 20.3.31 (2026-07-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [4ea787cd0](https://github.com/angular/angular-cli/commit/4ea787cd0dfc7226896643a6f8ccd99846c85c2c) | fix | bump undici to 7.28.0 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------ | +| [df5a2cc2b](https://github.com/angular/angular-cli/commit/df5a2cc2bcf8d65d834164984ee5456718dfb75e) | fix | bump vite to 7.3.6 | + + + + + +# 22.0.4 (2026-06-24) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [46185ceac](https://github.com/angular/angular-cli/commit/46185ceacc86cc04819b991621b7bb4e0d865b5a) | fix | aggregate parallel worker performance timings on the main thread | +| [5d7e29c41](https://github.com/angular/angular-cli/commit/5d7e29c41ed3fe0abe77dcdc894d5b7875293cb5) | perf | dispose builder result context early in non-watch mode | + + + + + +# 21.2.17 (2026-06-24) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------- | +| [9c366e8be](https://github.com/angular/angular-cli/commit/9c366e8be42495d1bf90033788b3beae77e5fc28) | fix | bump @babel/core to 7.29.7 | +| [6a06ce9e9](https://github.com/angular/angular-cli/commit/6a06ce9e9847b4ddd66b94b9ea4c6fbbfc28d066) | fix | bump esbuild to 0.28.1 | +| [637b84877](https://github.com/angular/angular-cli/commit/637b84877334de5acc6fcbe1ba4d62c7a6da420b) | fix | bump piscina to 5.2.0 | +| [e86d58a12](https://github.com/angular/angular-cli/commit/e86d58a128725eee9dd63dcd2ccd35b8ee4706f3) | fix | bump vite to 7.3.5 | + + + + + +# 20.3.30 (2026-06-24) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------- | +| [36adca99c](https://github.com/angular/angular-cli/commit/36adca99ce72d59e5909709f53e239b1e79d703e) | fix | bump @babel/core to 7.29.7 | +| [7f236cfea](https://github.com/angular/angular-cli/commit/7f236cfea9023a77c6ccaf1648f388678017ce1e) | fix | bump esbuild to 0.28.1 | +| [94fb3c122](https://github.com/angular/angular-cli/commit/94fb3c122a14a3a8540a24e22f2f8ff87d130cb5) | fix | bump piscina to 5.2.0 | +| [1f283a41c](https://github.com/angular/angular-cli/commit/1f283a41c921b194c67c5bca05d4b5cff1d91857) | fix | bump vite to 7.3.5 | + + + + + +# 22.0.3 (2026-06-18) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [0eddea898](https://github.com/angular/angular-cli/commit/0eddea898d3bf4da8c9c5c27ec2ee79916e8be87) | fix | remove default workspace vscode mcp.json configuration | + + + + + +# 21.2.16 (2026-06-17) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------- | +| [77c9047ac](https://github.com/angular/angular-cli/commit/77c9047acdf4b69ec6dd399af2dff7278e8014c7) | fix | update pacote to 21.5.1 | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [d052e97da](https://github.com/angular/angular-cli/commit/d052e97dafe2e85fbc072fd11025c803ebb80ef1) | fix | prioritize options over environment variables in AngularNodeAppEngine | + + + + + +# 20.3.29 (2026-06-17) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------- | +| [5f7c0328c](https://github.com/angular/angular-cli/commit/5f7c0328c3fe1a003ade44861d9aac98f485f014) | fix | update pacote to 21.5.1 | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [a75d78e68](https://github.com/angular/angular-cli/commit/a75d78e681b10436971d8071450b8cfd93b2d72a) | fix | prioritize options over environment variables in AngularNodeAppEngine | + + + + + +# 22.0.2 (2026-06-17) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [136fc2714](https://github.com/angular/angular-cli/commit/136fc27149af74263ef519007f0a74f9f85c5b4d) | fix | support registry metadata fetching under bun package manager | +| [2653dd5c7](https://github.com/angular/angular-cli/commit/2653dd5c7d47149b61bfe6edf4ab1281347e89dd) | perf | implement semaphore backpressure throttling in PackageManager | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [0b4a48add](https://github.com/angular/angular-cli/commit/0b4a48add9d7218e698af0db974bd597bed8a121) | perf | implement semaphore backpressure throttling in JavaScriptTransformer | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [d996a27e9](https://github.com/angular/angular-cli/commit/d996a27e9744b473a7db56f81871731b8bdce585) | fix | avoid caching non-SSG page lookups | +| [285a34e42](https://github.com/angular/angular-cli/commit/285a34e42f1cdc512468de0041b232c7190e7d7e) | fix | correct grammar in console warning for redirected location headers | +| [c8088a536](https://github.com/angular/angular-cli/commit/c8088a536c2c747a273e37be682643e1b35e2f75) | fix | prioritize options over environment variables in AngularNodeAppEngine | + + + + + +# 20.3.28 (2026-06-11) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------- | +| [e3d564667](https://github.com/angular/angular-cli/commit/e3d5646679215d9b73a72e04d87aa7848f2c01d2) | fix | fallback to deprecated versions when resolving ranges if no non-deprecated version is found | +| [f12e17025](https://github.com/angular/angular-cli/commit/f12e17025a262f9432afd58971c47aec7dbfab25) | fix | remove forceAuth and unscoped credential parsing | + + + + + +# 21.2.15 (2026-06-11) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [42ac0ed0f](https://github.com/angular/angular-cli/commit/42ac0ed0ff8b98862b9df1cef048f463ddc2cd85) | fix | remove forceAuth and unscoped credential parsing | +| [c7a7f1955](https://github.com/angular/angular-cli/commit/c7a7f1955619717ca775f730d67e3311047537f8) | fix | support registry metadata fetching under bun package manager | + + + + + +# 22.0.1 (2026-06-10) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------- | +| [b54e9a549](https://github.com/angular/angular-cli/commit/b54e9a549d30871f6017b1db4cf7a4ab5f3e02db) | fix | do not sort migrations of the same version alphabetically | +| [d33311612](https://github.com/angular/angular-cli/commit/d333116123c7d3d5e87713b7baac048b78f28517) | fix | fallback to local package.json for schematic detection on first run | +| [918102a93](https://github.com/angular/angular-cli/commit/918102a9373085394c41f10d9f5df3e3c17b263f) | fix | isolate temporary package installation from parent pnpm workspace | +| [b048b5f4a](https://github.com/angular/angular-cli/commit/b048b5f4a83d7b20095d79654b849808e7d58fdb) | fix | remove forceAuth and unscoped credential parsing | +| [277934035](https://github.com/angular/angular-cli/commit/277934035138c5af803e8daeebc2313f0a4cb5b3) | fix | validate registry option is a valid URL in ng add | +| [4510dae02](https://github.com/angular/angular-cli/commit/4510dae021ab25bb852eeed6415dbd52cfabfce5) | perf | optimize update schematic registry query counts by fetching package metadata lazily | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [c80012294](https://github.com/angular/angular-cli/commit/c8001229453211b37cd7bb12ed26a2deb9257fd5) | fix | fix browserMode option mapping in refactor-jasmine-vitest | +| [a9b6bd904](https://github.com/angular/angular-cli/commit/a9b6bd9042d6b859c384a6fc782541fca30dfb68) | fix | safely comment out multiline statements in refactor-jasmine-vitest | +| [12199df00](https://github.com/angular/angular-cli/commit/12199df00f2e3e8436ada13e04799e5825eb3f7b) | fix | use null objects and callbacks in karma-to-vitest migration | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [89d1be979](https://github.com/angular/angular-cli/commit/89d1be979f388d85e9c428bbf1df4e7fb4036dce) | fix | allow disabling Vitest isolation from builder | +| [d45b84be9](https://github.com/angular/angular-cli/commit/d45b84be9a607e49b391cb216cb6de7eca274931) | fix | exclude JSON imports from Vite dependency optimization | +| [e3cab4ddd](https://github.com/angular/angular-cli/commit/e3cab4dddade2538125e8a2f345f42c95e26aeae) | fix | prevent concurrent stylesheet bundling esbuild context leaks | +| [bd413b0eb](https://github.com/angular/angular-cli/commit/bd413b0eb156184ea432cbb7d4e6d7f6f70813ab) | fix | restrict application builder output paths to output directory | + + + # 22.0.0 (2026-06-03) @@ -14550,21 +15324,23 @@ Alan Agius, Charles Lyding, Joey Perrott, Keen Yee Liau, Luca Vazzano, Pankaj Pa A number of browser and server builder options have had their default values changed. The aim of these changes is to reduce the configuration complexity and support the new "production builds by default" initiative. **Browser builder** -| Option | Previous default value | New default value | -|----------------------------------------|---------------------------|-------------------| -| optimization | false | true | -| aot | false | true | -| buildOptimizer | false | true | -| sourceMap | true | false | -| extractLicenses | false | true | -| namedChunks | true | false | -| vendorChunk | true | false | + +| Option | Previous default value | New default value | +| --------------- | ---------------------- | ----------------- | +| optimization | false | true | +| aot | false | true | +| buildOptimizer | false | true | +| sourceMap | true | false | +| extractLicenses | false | true | +| namedChunks | true | false | +| vendorChunk | true | false | **Server builder** -| Option | Previous default value | New default value | -|---------------|------------------------|-------------------| -| optimization | false | true | -| sourceMap | true | false | + +| Option | Previous default value | New default value | +| ------------ | ---------------------- | ----------------- | +| optimization | false | true | +| sourceMap | true | false | (cherry picked from commit 0a74d0d28daf68510459ed73ef048c91bfcabbbc) @@ -15585,21 +16361,23 @@ Alan Agius, Charles Lyding, Joey Perrott, Cédric Exbrayat, Doug Parker, Joshua A number of browser and server builder options have had their default values changed. The aim of these changes is to reduce the configuration complexity and support the new "production builds by default" initiative. **Browser builder** -| Option | Previous default value | New default value | -|----------------------------------------|---------------------------|-------------------| -| optimization | false | true | -| aot | false | true | -| buildOptimizer | false | true | -| sourceMap | true | false | -| extractLicenses | false | true | -| namedChunks | true | false | -| vendorChunk | true | false | + +| Option | Previous default value | New default value | +| --------------- | ---------------------- | ----------------- | +| optimization | false | true | +| aot | false | true | +| buildOptimizer | false | true | +| sourceMap | true | false | +| extractLicenses | false | true | +| namedChunks | true | false | +| vendorChunk | true | false | **Server builder** -| Option | Previous default value | New default value | -|---------------|------------------------|-------------------| -| optimization | false | true | -| sourceMap | true | false | + +| Option | Previous default value | New default value | +| ------------ | ---------------------- | ----------------- | +| optimization | false | true | +| sourceMap | true | false | (cherry picked from commit 0a74d0d28daf68510459ed73ef048c91bfcabbbc) diff --git a/MODULE.bazel b/MODULE.bazel index 0da20dc77a64..9bfc8c1b806d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -6,49 +6,49 @@ module( bazel_dep(name = "platforms", version = "1.1.0") bazel_dep(name = "yq.bzl", version = "0.3.6") -bazel_dep(name = "rules_nodejs", version = "6.7.4") -bazel_dep(name = "aspect_rules_js", version = "3.2.0") -bazel_dep(name = "aspect_rules_ts", version = "3.8.10") -bazel_dep(name = "rules_pkg", version = "1.2.0") -bazel_dep(name = "rules_cc", version = "0.2.19") +bazel_dep(name = "rules_nodejs", version = "6.7.5") +bazel_dep(name = "aspect_rules_js", version = "3.4.0") +bazel_dep(name = "aspect_rules_ts", version = "3.10.0") +bazel_dep(name = "rules_pkg", version = "1.3.0") +bazel_dep(name = "rules_cc", version = "0.2.22") bazel_dep(name = "jq.bzl", version = "0.6.1") -bazel_dep(name = "bazel_lib", version = "3.3.1") -bazel_dep(name = "bazel_skylib", version = "1.9.0") -bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0") +bazel_dep(name = "bazel_lib", version = "3.7.1") +bazel_dep(name = "bazel_skylib", version = "1.9.2") +bazel_dep(name = "aspect_rules_esbuild", version = "0.27.0") bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4") bazel_dep(name = "rules_angular") git_override( module_name = "rules_angular", - commit = "02e0d38eb29b2721128ca7e93ee694e68d4f06f6", + commit = "334ed4f0bf82cfcf21b1d8bd501d2294b186061e", remote = "https://github.com/angular/rules_angular.git", ) bazel_dep(name = "devinfra") git_override( module_name = "devinfra", - commit = "e78f06f32bc0780cfda374af155a1b5dc84fa14d", + commit = "6d918a6dd670e28afd4ba54d00b805d4aba43dca", remote = "https://github.com/angular/dev-infra.git", ) bazel_dep(name = "rules_browsers") git_override( module_name = "rules_browsers", - commit = "c7e596a4a34f651113c42a9da3f82acc0a01484e", + commit = "dd75910a556e309d6f475d54214551be919a21d1", remote = "https://github.com/angular/rules_browsers.git", ) node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node") node.toolchain( node_repositories = { - "22.22.3-darwin_arm64": ("node-v22.22.3-darwin-arm64.tar.gz", "node-v22.22.3-darwin-arm64", "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207"), - "22.22.3-darwin_amd64": ("node-v22.22.3-darwin-x64.tar.gz", "node-v22.22.3-darwin-x64", "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec"), - "22.22.3-linux_arm64": ("node-v22.22.3-linux-arm64.tar.xz", "node-v22.22.3-linux-arm64", "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7"), - "22.22.3-linux_ppc64le": ("node-v22.22.3-linux-ppc64le.tar.xz", "node-v22.22.3-linux-ppc64le", "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754"), - "22.22.3-linux_s390x": ("node-v22.22.3-linux-s390x.tar.xz", "node-v22.22.3-linux-s390x", "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617"), - "22.22.3-linux_amd64": ("node-v22.22.3-linux-x64.tar.xz", "node-v22.22.3-linux-x64", "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f"), - "22.22.3-windows_amd64": ("node-v22.22.3-win-x64.zip", "node-v22.22.3-win-x64", "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33"), + "24.19.0-darwin_arm64": ("node-v24.19.0-darwin-arm64.tar.gz", "node-v24.19.0-darwin-arm64", "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d"), + "24.19.0-darwin_amd64": ("node-v24.19.0-darwin-x64.tar.gz", "node-v24.19.0-darwin-x64", "d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316"), + "24.19.0-linux_arm64": ("node-v24.19.0-linux-arm64.tar.xz", "node-v24.19.0-linux-arm64", "01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc"), + "24.19.0-linux_ppc64le": ("node-v24.19.0-linux-ppc64le.tar.xz", "node-v24.19.0-linux-ppc64le", "c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3"), + "24.19.0-linux_s390x": ("node-v24.19.0-linux-s390x.tar.xz", "node-v24.19.0-linux-s390x", "a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4"), + "24.19.0-linux_amd64": ("node-v24.19.0-linux-x64.tar.xz", "node-v24.19.0-linux-x64", "14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647"), + "24.19.0-windows_amd64": ("node-v24.19.0-win-x64.zip", "node-v24.19.0-win-x64", "57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73"), }, - node_version = "22.22.3", + node_version = "24.19.0", ) use_repo( node, @@ -131,8 +131,8 @@ use_repo( pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm") pnpm.pnpm( name = "pnpm", - pnpm_version = "11.5.2", - pnpm_version_integrity = "sha512-ccYx44IGbvwlYl1c8CkHXeB7YbN/bic1D72Esb2lhkyMGWetwoB3a0XDCnFcA1mjvgj+9C1bsJ4rmQKZeWkpFg==", + pnpm_version = "11.22.0", + pnpm_version_integrity = "sha512-H/hwxMYTPf2I+yr8Rt0T1H8JyXlLQ4xv20fKmMrzvBY4HuC+k6CRuOOCTPAfiJ9G19niCRD7C+GrD7W6qA3WIQ==", ) use_repo(pnpm, "pnpm") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index dc32a6881872..b1c759780a9a 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -18,19 +18,19 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", - "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.26.0/MODULE.bazel": "6c902d97038c3ab07b6c4e67c97abc61b20182fcfa84fa7dee82fc724f12e455", - "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.26.0/source.json": "4cc3ece7ab661bb391a9e24fe55c4b567d60a9ea9d9e91d772dad373cbcb6217", + "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.27.0/MODULE.bazel": "877dafc0b925f8af19e8bc2abed04a757bb565c57c1866e8851ac4d15ed5e6d2", + "https://bcr.bazel.build/modules/aspect_rules_esbuild/0.27.0/source.json": "21f8738b3e62310ef43b7cef4284e1bafd69bd8e4e50251b71b20bbfed4372d8", "https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.4/MODULE.bazel": "fbb819eb8b7e5d7f67fdd38f7cecb413e287594cd666ce192c72c8828527775a", "https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.4/source.json": "81ffb708333cd98ec3c0b4cc004f4d5cf92a16914b5196a2892c45141bba7cff", "https://bcr.bazel.build/modules/aspect_rules_js/2.0.0/MODULE.bazel": "b45b507574aa60a92796e3e13c195cd5744b3b8aff516a9c0cb5ae6a048161c5", - "https://bcr.bazel.build/modules/aspect_rules_js/3.0.3/MODULE.bazel": "28a30e8fc33bf64a67835d64d124f6e05a7d59648dcb27b110fb3502f761e503", - "https://bcr.bazel.build/modules/aspect_rules_js/3.2.0/MODULE.bazel": "c3d34345c51a4d90cf678d3b251c6f345b63d5e2e6b0253dbdda467a01efc31f", - "https://bcr.bazel.build/modules/aspect_rules_js/3.2.0/source.json": "3538ab18ac72ba3eaa3db0e4ec5171e04322768723afaa1149c0d822fe5411e2", - "https://bcr.bazel.build/modules/aspect_rules_ts/3.8.10/MODULE.bazel": "a17a49a21226fc90163a29b3d6eac56703697205530b8d5cc38b3c074dbac039", - "https://bcr.bazel.build/modules/aspect_rules_ts/3.8.10/source.json": "745c8dba237b4088409800143241bbb138e7ef37a359bd81a250c2c423f380ce", + "https://bcr.bazel.build/modules/aspect_rules_js/3.4.0/MODULE.bazel": "88844ac411e1961f4574a92f3c5be5b20d1c6997778c6b88316c5c3b4b60e284", + "https://bcr.bazel.build/modules/aspect_rules_js/3.4.0/source.json": "85e5822f00dcbe64a1eda1324119e289c8c03cacb5c3695dffee16397b529078", + "https://bcr.bazel.build/modules/aspect_rules_ts/3.10.0/MODULE.bazel": "69d06f57f30f4a2b6e53471584a9559d3b7cd7f891e1699876991230c7cabb95", + "https://bcr.bazel.build/modules/aspect_rules_ts/3.10.0/source.json": "56f28a3ddb55ceaaf57a1ef8d7195136789ca1d72d0c8a6a9eeaad313be4099d", "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679", "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183", - "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/source.json": "605086bbc197743a0d360f7ddc550a1d4dfa0441bc807236e17170f636153348", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/MODULE.bazel": "f31aa84151d31e98cffd43eb7217ccff5ec52bdd5f2d10db8f053aeb23342eca", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/source.json": "d027d264e6b6e7fc421e38189f4374fcd14a67e0bd6e0e705de8c2185c3787e1", "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.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", @@ -44,15 +44,15 @@ "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.41.0/MODULE.bazel": "6e0f87fafed801273c371d41e22a15a6f8abf83fdd7f87d5e44ad317b94433d0", - "https://bcr.bazel.build/modules/bazel_features/1.47.0/MODULE.bazel": "e34df3cb35b1684cfa69923a61ae3803595babd3942cd306a488d51400886b30", - "https://bcr.bazel.build/modules/bazel_features/1.47.0/source.json": "4ba0b5138327f2d73352a51547a4e49a0a828ef400e046b15334d8905bf6b7ff", + "https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e", + "https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18", "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", - "https://bcr.bazel.build/modules/bazel_lib/3.2.2/MODULE.bazel": "e2c890c8a515d6bca9c66d47718aa9e44b458fde64ec7204b8030bf2d349058c", - "https://bcr.bazel.build/modules/bazel_lib/3.3.1/MODULE.bazel": "732a0d516cf6400d9b3136e4356258aef1bf91de8d5240f87f0112f098920c1d", - "https://bcr.bazel.build/modules/bazel_lib/3.3.1/source.json": "8e5175d7b4125a39b8941d01e38039934d058e03804f46a2b8fd7ae6316b1ce2", + "https://bcr.bazel.build/modules/bazel_lib/3.7.0/MODULE.bazel": "d7c10ed67f0f7f1fda179db8f86c22642581bd614882e1a50545fbe069525173", + "https://bcr.bazel.build/modules/bazel_lib/3.7.1/MODULE.bazel": "b6fd9b2f8fab956420c11836f416efac4a70e20804ae384ebe62773a4ed70046", + "https://bcr.bazel.build/modules/bazel_lib/3.7.1/source.json": "635fdaa28b50c04febc5e60ef51bc913d3bc87bfbaac7045449273c2341648cb", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -68,7 +68,8 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", + "https://bcr.bazel.build/modules/bazel_skylib/1.9.2/MODULE.bazel": "8c51259b0f4481475586dbfede7591e57b75702e637f840f6138eec80c34b270", + "https://bcr.bazel.build/modules/bazel_skylib/1.9.2/source.json": "41cbde7546542dee2f26e3f10bf1c4ac57909943194b7ac48cc5238a01893aa8", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", @@ -125,9 +126,10 @@ "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.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", - "https://bcr.bazel.build/modules/rules_cc/0.2.19/MODULE.bazel": "d5e0f05b63273281a16654eb6b1a8742a75ec153ac8b4f0419949d6e401e46f0", - "https://bcr.bazel.build/modules/rules_cc/0.2.19/source.json": "1ef48cdbd7aa6238015189b582d3d74ef0cbea3cb3e2cb259d782463f570c14a", + "https://bcr.bazel.build/modules/rules_cc/0.2.22/MODULE.bazel": "94df4328edef9e44d38de5e73b037cd348e75e7ae55f4e21bf07878c41a31ebb", + "https://bcr.bazel.build/modules/rules_cc/0.2.22/source.json": "b2d6d6f9c332ce269ad75b89c6f3168d809a66173c9040210fb9bcc733ab42fa", "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", @@ -164,12 +166,12 @@ "https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e", "https://bcr.bazel.build/modules/rules_nodejs/6.5.0/MODULE.bazel": "546d0cf79f36f9f6e080816045f97234b071c205f4542e3351bd4424282a8810", "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056", - "https://bcr.bazel.build/modules/rules_nodejs/6.7.4/MODULE.bazel": "e6a241a55c82e999145553d2e00a08fc6ebadf62b63d108fb5e984696ffd0bd2", - "https://bcr.bazel.build/modules/rules_nodejs/6.7.4/source.json": "34e7a8a3b4c8d630ac0e0492b3fed9dba41fe008a0edf220b7d88fa38ac53698", + "https://bcr.bazel.build/modules/rules_nodejs/6.7.5/MODULE.bazel": "97e6794043821d23c013baa4a50fd1c599f2e6ae92b06e2c5f1cd7074fd83e7c", + "https://bcr.bazel.build/modules/rules_nodejs/6.7.5/source.json": "d60ee5a76258b1c8f99545ed24172b44d43ba64ca1a2dfc04371ef203df19fdf", "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", - "https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e", - "https://bcr.bazel.build/modules/rules_pkg/1.2.0/source.json": "9062e00845bf91a4247465d371baa837adf9b6ff44c542f73ba084f07667e1dc", + "https://bcr.bazel.build/modules/rules_pkg/1.3.0/MODULE.bazel": "ae0bdefbacc990c91f843206c90cf0f4be620639a5bf22119043599ba86d51a3", + "https://bcr.bazel.build/modules/rules_pkg/1.3.0/source.json": "58ae84c545141762f7c434c0ce78bfb55ef0be08d84863e7f0503fff135fe4e2", "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", @@ -183,8 +185,8 @@ "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", - "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", - "https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917", + "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", + "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "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", @@ -198,14 +200,12 @@ "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/tar.bzl/0.10.4/MODULE.bazel": "e8f9ff79199e8d9eaad7f1b0a77ad74b30bb82d794b87d8ca942bead5de83ae9", - "https://bcr.bazel.build/modules/tar.bzl/0.10.5/MODULE.bazel": "e964c1f07f7174478d43be7177f364640e24956d5994bd39d48548cc6a436e2c", - "https://bcr.bazel.build/modules/tar.bzl/0.10.5/source.json": "6703dc57a6c7118722ac452ffb0a05f6e205408b810c6aaf25741a635d5f4b89", + "https://bcr.bazel.build/modules/tar.bzl/0.10.8/MODULE.bazel": "443884cabe241f640cfef256b1de2ccb116752895532900a4be9500e1124323f", + "https://bcr.bazel.build/modules/tar.bzl/0.10.8/source.json": "4173be64b38e471d92d2eb139a6496311de6de75e710f04edce78c43122c5419", "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", - "https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", - "https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6", "https://bcr.bazel.build/modules/yq.bzl/0.3.4/MODULE.bazel": "d3a270662f5d766cd7229732d65a5a5bc485240c3007343dd279edfb60c9ae27", "https://bcr.bazel.build/modules/yq.bzl/0.3.6/MODULE.bazel": "985c2a0cb4ad9994bb0e33cc7fae931c91105eeefe3faa355b8f4c258d0607c0", "https://bcr.bazel.build/modules/yq.bzl/0.3.6/source.json": "678aaf6e291164f3cd761bb3e872e8a151248f413dbb63c5524a50b82a5bc890", @@ -219,8 +219,8 @@ "moduleExtensions": { "@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": { "general": { - "bzlTransitiveDigest": "8bNoUkbLk1vCab3fXdV3RN1UoOjEcHlo/bk9XxZ7e3A=", - "usagesDigest": "LSQ+zZp7JNgnBONTxxXnwGr4NTh2qtQYk7qwXXz5qWo=", + "bzlTransitiveDigest": "eGsGosHZTHOEZx11j7nDLNMHxCkESoe1oypcL3+G6GI=", + "usagesDigest": "LZ71sshnfqI8R/HeWwBTwawxIa7KnitayBl2hYrqPo4=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -432,90 +432,34 @@ ] } }, - "@@aspect_rules_ts+//ts:extensions.bzl%ext": { - "general": { - "bzlTransitiveDigest": "iSTT2Cf1ed8ku80yZFxvYxZB6Lpy6M8zJBz98hxdxcU=", - "usagesDigest": "QQqokxpCVnBJntX7dhxnf/c13LeWUQxDTUnILYCp4Jc=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "angular_cli_npm_typescript": { - "repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version", - "attributes": { - "version": "6.0.3", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "urls": [ - "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" - ] - } - }, - "rules_angular_npm_typescript": { - "repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version", - "attributes": { - "version": "6.0.3", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "urls": [ - "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" - ] - } - }, - "npm_typescript": { - "repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version", - "attributes": { - "version": "6.0.3", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "urls": [ - "https://registry.npmjs.org/typescript/-/typescript-{}.tgz" - ] - } - }, - "npm_rules_browsers_typescript": { - "repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version", - "attributes": { - "version": "6.0.3", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "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": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=", - "usagesDigest": "bsm1cFI7hhC0zj7wvllOScDJKporKySjStBNr8xm+dY=", + "bzlTransitiveDigest": "4w9RM0xjdKo1crk5zL20a/TuhqO0P1z1LsuXDneBXD4=", + "usagesDigest": "VoGyqZ+r/3juGZUfSXD4FAe965Z3XS4WANRrYH79+1Q=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, - "envVariables": {}, + "envVariables": { + "ASPECT_TOOLS_TELEMETRY_TEST": null + }, "generatedRepoSpecs": { "aspect_tools_telemetry_report": { "repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository", "attributes": { "deps": { - "aspect_rules_js": "3.2.0", - "aspect_rules_ts": "3.8.10", - "aspect_rules_esbuild": "0.26.0", + "aspect_rules_js": "3.4.0", + "aspect_rules_ts": "3.10.0", + "aspect_rules_esbuild": "0.27.0", "aspect_rules_jasmine": "2.0.4", - "aspect_tools_telemetry": "0.3.3" - } + "aspect_tools_telemetry": "0.4.2" + }, + "last_notice": 0 } } }, + "moduleExtensionMetadata": { + "useAllRepos": "NO", + "reproducible": false + }, "recordedRepoMappingEntries": [ [ "aspect_tools_telemetry+", @@ -592,7 +536,7 @@ }, "@@rules_browsers+//browsers:extensions.bzl%browsers": { "general": { - "bzlTransitiveDigest": "lQlRwI+HZTXohoZ41oufeudyNMhYrxQizk07ADXimPI=", + "bzlTransitiveDigest": "85AglD+Q1+ncEIApuW5Ng/2cY4OkSMPrOZ05Jp4CTmk=", "usagesDigest": "FmXYJVoVJlnfUU8x8gObSvu4qWcco/9Faw61aC/wBF0=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -601,9 +545,9 @@ "rules_browsers_chrome_linux": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "fd4df6dcadda8641af4f5d79137b23f1b1271a1cfcc0b96f52cb1b8bcd7c01d5", + "sha256": "1a20882bf1a4f9d1167171b6905efe359bb229be1ce636ae9c931f3d285fbabe", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/linux64/chrome-headless-shell-linux64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/linux64/chrome-headless-shell-linux64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-linux64/chrome-headless-shell" @@ -619,9 +563,9 @@ "rules_browsers_chrome_mac": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "4136f1a9524ac5130dd29fe9eef82094139f079520af0b28f0d360f4f3f550ed", + "sha256": "1b0e0a078fc62c2b9f8cf837d41461084bd4064cf50cfc15ff84447a1f1b8dab", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/mac-x64/chrome-headless-shell-mac-x64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-x64/chrome-headless-shell-mac-x64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-x64/chrome-headless-shell" @@ -637,9 +581,9 @@ "rules_browsers_chrome_mac_arm": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "f2cb9d22ad425a23e1d58dab7ac91e546386099a5acb811ca5749d40bf2d9886", + "sha256": "7438714da3696778b697b277a2df1d9701b99819ae58f5ab809b1009933677ec", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/mac-arm64/chrome-headless-shell-mac-arm64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-arm64/chrome-headless-shell-mac-arm64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-arm64/chrome-headless-shell" @@ -655,9 +599,9 @@ "rules_browsers_chrome_win64": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "684055979848753211b24215d25993c453ee26cdb2d84d7bfb24f84118e02198", + "sha256": "4349c800be1f063adc0f1a7e820d024b43ea73917a6893a3f6b3797a5200d49b", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/win64/chrome-headless-shell-win64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/win64/chrome-headless-shell-win64.zip" ], "named_files": { "CHROME-HEADLESS-SHELL": "chrome-headless-shell-win64/chrome-headless-shell.exe" @@ -673,9 +617,9 @@ "rules_browsers_chromedriver_linux": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "d9e12133a8af192f8a28f2ef0d86ac54f7458c71e28bddf95947d5636472d508", + "sha256": "d275f01236831bd44c6b6712ba5bec0a028b1ce9473a6182aceae6657740a3d3", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/linux64/chromedriver-linux64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/linux64/chromedriver-linux64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-linux64/chromedriver" @@ -689,9 +633,9 @@ "rules_browsers_chromedriver_mac": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "7432080f1c3fc8131d58870e93ca9a96c807fedc04deaaf5c1a60942f3c1d321", + "sha256": "cb331889191aa8ab11be6d5a3cc4de760f8cf63dbea5d1f22c80cf16ae68e46f", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/mac-x64/chromedriver-mac-x64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-x64/chromedriver-mac-x64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-mac-x64/chromedriver" @@ -705,9 +649,9 @@ "rules_browsers_chromedriver_mac_arm": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "0bfbda9352bbae72755b463f656a6f335481bc02be1ba8edfe1fb74a08ce0d7b", + "sha256": "1584dc9ae35f4a22c4ba4b142335d91d6d3096813395d3acaf135fb6e716ce8e", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/mac-arm64/chromedriver-mac-arm64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/mac-arm64/chromedriver-mac-arm64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-mac-arm64/chromedriver" @@ -721,9 +665,9 @@ "rules_browsers_chromedriver_win64": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "766efed7d9c6a3aa89733d59b64316849775002bdde7e247318c8129bbd6d747", + "sha256": "061b4d1be9eb2a11ed036b3e562b565ebfd257fcd7cda3681a58e63bff5dd6d0", "urls": [ - "https://storage.googleapis.com/chrome-for-testing-public/151.0.7881.0/win64/chromedriver-win64.zip" + "https://storage.googleapis.com/chrome-for-testing-public/154.0.8013.0/win64/chromedriver-win64.zip" ], "named_files": { "CHROMEDRIVER": "chromedriver-win64/chromedriver.exe" @@ -737,9 +681,9 @@ "rules_browsers_firefox_linux": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "8ff8557a5ca3903ebbc1d18570684075f623465be5e362d63a95b3acc523f824", + "sha256": "7665cd49ab13417270748325838e565136adbc76d41bbd76fb24d15a0cc7792b", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/151.0/linux-x86_64/en-US/firefox-151.0.tar.xz" + "https://archive.mozilla.org/pub/firefox/releases/154.0/linux-x86_64/en-US/firefox-154.0.tar.xz" ], "named_files": { "FIREFOX": "firefox/firefox" @@ -753,9 +697,9 @@ "rules_browsers_firefox_mac": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "5a56ebd8f0f8cec94a86e04c6793e1d1502b40206f5d4c86fff5b7a270e84f6b", + "sha256": "b0295d3b77ec632a60282cf2b9770e1dada085879d00e43d8d23fddf5015e06a", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/151.0/mac/en-US/Firefox%20151.0.dmg" + "https://archive.mozilla.org/pub/firefox/releases/154.0/mac/en-US/Firefox%20154.0.dmg" ], "named_files": { "FIREFOX": "Firefox.app/Contents/MacOS/firefox" @@ -769,9 +713,9 @@ "rules_browsers_firefox_mac_arm": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "5a56ebd8f0f8cec94a86e04c6793e1d1502b40206f5d4c86fff5b7a270e84f6b", + "sha256": "b0295d3b77ec632a60282cf2b9770e1dada085879d00e43d8d23fddf5015e06a", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/151.0/mac/en-US/Firefox%20151.0.dmg" + "https://archive.mozilla.org/pub/firefox/releases/154.0/mac/en-US/Firefox%20154.0.dmg" ], "named_files": { "FIREFOX": "Firefox.app/Contents/MacOS/firefox" @@ -785,9 +729,9 @@ "rules_browsers_firefox_win64": { "repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo", "attributes": { - "sha256": "358381238a840831da8a6cda16721692df91a74bc44847ff0285da12de788a8d", + "sha256": "6b3c996d27b50dca8e5b6dd2956233cc6bf4720cea3f0847a9423c2cd6759e73", "urls": [ - "https://archive.mozilla.org/pub/firefox/releases/151.0/win64/en-US/Firefox%20Setup%20151.0.exe" + "https://archive.mozilla.org/pub/firefox/releases/154.0/win64/en-US/Firefox%20Setup%20154.0.exe" ], "named_files": { "FIREFOX": "core/firefox.exe" @@ -949,4465 +893,351 @@ ] } }, - "@@rules_nodejs+//nodejs:extensions.bzl%node": { + "@@rules_python+//python/extensions:config.bzl%config": { "general": { - "bzlTransitiveDigest": "oZFClfRhTTwsYzpxVPkOpOt/r0+OzEfEV37au0jFZ0s=", - "usagesDigest": "00+3uj4j6s/drSt+SUad8w/R4Rp2/GHGBsCgrKvF7mE=", + "bzlTransitiveDigest": "1eE/aNv0So57EZMzcxgDSCFkzlydiaC4TpHowQfrcBk=", + "usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, "generatedRepoSpecs": { - "nodejs_linux_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "rules_python_internal": { + "repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_amd64" + "transition_setting_generators": {}, + "transition_settings": [] } }, - "nodejs_linux_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__build": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_arm64" + "url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", + "sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_linux_s390x": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__click": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_s390x" + "url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", + "sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_linux_ppc64le": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__colorama": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_ppc64le" + "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", + "sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_darwin_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__importlib_metadata": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "darwin_amd64" + "url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl", + "sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_darwin_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__installer": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "darwin_arm64" + "url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl", + "sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_windows_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__more_itertools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "windows_amd64" + "url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", + "sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_windows_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__packaging": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "windows_arm64" + "url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", + "sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "pypi__pep517": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "user_node_repository_name": "nodejs" + "url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl", + "sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_host": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "pypi__pip": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "user_node_repository_name": "nodejs" + "url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl", + "sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "nodejs_toolchains": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo", + "pypi__pip_tools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "user_node_repository_name": "nodejs" + "url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", + "sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "node22_linux_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__pyproject_hooks": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_amd64" + "url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", + "sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "node22_linux_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__setuptools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_arm64" + "url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", + "sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "node22_linux_s390x": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__tomli": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_s390x" + "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", + "sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "node22_linux_ppc64le": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__wheel": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "linux_ppc64le" + "url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl", + "sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } }, - "node22_darwin_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "pypi__zipp": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "darwin_amd64" + "url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl", + "sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" } - }, - "node22_darwin_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_python+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_python+", + "pypi__build", + "rules_python++config+pypi__build" + ], + [ + "rules_python+", + "pypi__click", + "rules_python++config+pypi__click" + ], + [ + "rules_python+", + "pypi__colorama", + "rules_python++config+pypi__colorama" + ], + [ + "rules_python+", + "pypi__importlib_metadata", + "rules_python++config+pypi__importlib_metadata" + ], + [ + "rules_python+", + "pypi__installer", + "rules_python++config+pypi__installer" + ], + [ + "rules_python+", + "pypi__more_itertools", + "rules_python++config+pypi__more_itertools" + ], + [ + "rules_python+", + "pypi__packaging", + "rules_python++config+pypi__packaging" + ], + [ + "rules_python+", + "pypi__pep517", + "rules_python++config+pypi__pep517" + ], + [ + "rules_python+", + "pypi__pip", + "rules_python++config+pypi__pip" + ], + [ + "rules_python+", + "pypi__pip_tools", + "rules_python++config+pypi__pip_tools" + ], + [ + "rules_python+", + "pypi__pyproject_hooks", + "rules_python++config+pypi__pyproject_hooks" + ], + [ + "rules_python+", + "pypi__setuptools", + "rules_python++config+pypi__setuptools" + ], + [ + "rules_python+", + "pypi__tomli", + "rules_python++config+pypi__tomli" + ], + [ + "rules_python+", + "pypi__wheel", + "rules_python++config+pypi__wheel" + ], + [ + "rules_python+", + "pypi__zipp", + "rules_python++config+pypi__zipp" + ] + ] + } + }, + "@@rules_python+//python/uv:uv.bzl%uv": { + "general": { + "bzlTransitiveDigest": "ijW9KS7qsIY+yBVvJ+Nr1mzwQox09j13DnE3iIwaeTM=", + "usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "uv": { + "repoRuleId": "@@rules_python+//python/uv/private:uv_toolchains_repo.bzl%uv_toolchains_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" + "toolchain_type": "'@@rules_python+//python/uv:uv_toolchain_type'", + "toolchain_names": [ + "none" ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "darwin_arm64" - } - }, - "node22_windows_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" + "toolchain_implementations": { + "none": "'@@rules_python+//python:none'" + }, + "toolchain_compatible_with": { + "none": [ + "@platforms//:incompatible" ] }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "windows_amd64" + "toolchain_target_settings": {} } - }, - "node22_windows_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_python+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_python+", + "platforms", + "platforms" + ] + ] + } + }, + "@@yq.bzl+//yq:extensions.bzl%yq": { + "general": { + "bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=", + "usagesDigest": "9kbJ6OfvQpAM3otgkGgS5xpOHOoVgV5ZJ9jSgMgUG2g=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "yq_darwin_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "22.22.3-darwin_arm64": [ - "node-v22.22.3-darwin-arm64.tar.gz", - "node-v22.22.3-darwin-arm64", - "0da7ff74ef8611328c8212f17943368713a2ad953fb7d89a8c8a0eae87c23207" - ], - "22.22.3-darwin_amd64": [ - "node-v22.22.3-darwin-x64.tar.gz", - "node-v22.22.3-darwin-x64", - "45830ba752fa0d892c6dcd640946669801293cac820a33591ded40ac075198ec" - ], - "22.22.3-linux_arm64": [ - "node-v22.22.3-linux-arm64.tar.xz", - "node-v22.22.3-linux-arm64", - "1c4a9933a5e45bc88f54f70b5f91232c127ec49f1a5989d23fb85824c7adf9b7" - ], - "22.22.3-linux_ppc64le": [ - "node-v22.22.3-linux-ppc64le.tar.xz", - "node-v22.22.3-linux-ppc64le", - "edb5478071bd1375e80195ca52f72823998bb5141b1a09e68bc54b3e2eb67754" - ], - "22.22.3-linux_s390x": [ - "node-v22.22.3-linux-s390x.tar.xz", - "node-v22.22.3-linux-s390x", - "ce398c057830d57a24c458177279a17bc51742d5c22dd4cbe97b10dbd43f2617" - ], - "22.22.3-linux_amd64": [ - "node-v22.22.3-linux-x64.tar.xz", - "node-v22.22.3-linux-x64", - "2e5d13569282d016861fae7c8f935e741693c269101a5bebcf761a5376d1f99f" - ], - "22.22.3-windows_amd64": [ - "node-v22.22.3-win-x64.zip", - "node-v22.22.3-win-x64", - "6c8d54f635feff4df76c2ca80f45332eb2ff57d25226edce36592e51a177ee33" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "22.22.3", - "include_headers": false, - "platform": "windows_arm64" + "platform": "darwin_amd64", + "version": "4.45.2" } }, - "node22": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "yq_darwin_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "user_node_repository_name": "node22" + "platform": "darwin_arm64", + "version": "4.45.2" } }, - "node22_host": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "yq_linux_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "user_node_repository_name": "node22" + "platform": "linux_amd64", + "version": "4.45.2" } }, - "node22_toolchains": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo", + "yq_linux_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "user_node_repository_name": "node22" + "platform": "linux_arm64", + "version": "4.45.2" } }, - "node24_linux_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "yq_linux_s390x": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "linux_amd64" + "platform": "linux_s390x", + "version": "4.45.2" } }, - "node24_linux_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "yq_linux_riscv64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "linux_arm64" + "platform": "linux_riscv64", + "version": "4.45.2" } }, - "node24_linux_s390x": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "yq_linux_ppc64le": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "linux_s390x" + "platform": "linux_ppc64le", + "version": "4.45.2" } }, - "node24_linux_ppc64le": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "yq_windows_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "linux_ppc64le" + "platform": "windows_amd64", + "version": "4.45.2" } }, - "node24_darwin_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "yq_windows_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "darwin_amd64" - } - }, - "node24_darwin_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "darwin_arm64" - } - }, - "node24_windows_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "windows_amd64" - } - }, - "node24_windows_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "24.15.0-darwin_arm64": [ - "node-v24.15.0-darwin-arm64.tar.gz", - "node-v24.15.0-darwin-arm64", - "372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4" - ], - "24.15.0-darwin_amd64": [ - "node-v24.15.0-darwin-x64.tar.gz", - "node-v24.15.0-darwin-x64", - "ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b" - ], - "24.15.0-linux_arm64": [ - "node-v24.15.0-linux-arm64.tar.xz", - "node-v24.15.0-linux-arm64", - "f3d5a797b5d210ce8e2cb265544c8e482eaedcb8aa409a8b46da7e8595d0dda0" - ], - "24.15.0-linux_ppc64le": [ - "node-v24.15.0-linux-ppc64le.tar.xz", - "node-v24.15.0-linux-ppc64le", - "6a6560a27bd2817013c28c3d917bfe9eebf26bbd4b1d88475190f216cc411fbb" - ], - "24.15.0-linux_s390x": [ - "node-v24.15.0-linux-s390x.tar.xz", - "node-v24.15.0-linux-s390x", - "940d4cbfadf736b34519630a05d144c09f8a5aca291a802f2f559ee1562f6f24" - ], - "24.15.0-linux_amd64": [ - "node-v24.15.0-linux-x64.tar.xz", - "node-v24.15.0-linux-x64", - "472655581fb851559730c48763e0c9d3bc25975c59d518003fc0849d3e4ba0f6" - ], - "24.15.0-windows_amd64": [ - "node-v24.15.0-win-x64.zip", - "node-v24.15.0-win-x64", - "cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "24.15.0", - "include_headers": false, - "platform": "windows_arm64" - } - }, - "node24": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", - "attributes": { - "user_node_repository_name": "node24" - } - }, - "node24_host": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", - "attributes": { - "user_node_repository_name": "node24" - } - }, - "node24_toolchains": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo", - "attributes": { - "user_node_repository_name": "node24" - } - }, - "node26_linux_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "linux_amd64" - } - }, - "node26_linux_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "linux_arm64" - } - }, - "node26_linux_s390x": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "linux_s390x" - } - }, - "node26_linux_ppc64le": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "linux_ppc64le" - } - }, - "node26_darwin_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "darwin_amd64" - } - }, - "node26_darwin_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "darwin_arm64" - } - }, - "node26_windows_amd64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "windows_amd64" - } - }, - "node26_windows_arm64": { - "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", - "attributes": { - "node_download_auth": {}, - "node_repositories": { - "26.0.0-darwin_arm64": [ - "node-v26.0.0-darwin-arm64.tar.gz", - "node-v26.0.0-darwin-arm64", - "dcee8564c1a9342f9594dd5e52d533894dfef6b85aa771bbbb870baa3c403235" - ], - "26.0.0-darwin_amd64": [ - "node-v26.0.0-darwin-x64.tar.gz", - "node-v26.0.0-darwin-x64", - "f488ab543fe202d8a2d56e661682117d3c56903a2bf64f2ec1ff7bd421cfd875" - ], - "26.0.0-linux_arm64": [ - "node-v26.0.0-linux-arm64.tar.xz", - "node-v26.0.0-linux-arm64", - "f0f94e55142149a4d34634dc3d7e103921d898512dd0cef995ecb62c5ebd3f29" - ], - "26.0.0-linux_ppc64le": [ - "node-v26.0.0-linux-ppc64le.tar.xz", - "node-v26.0.0-linux-ppc64le", - "4b7f76967a93fea8cda11554f2a7904744afaef65dc3f48c345e99828f50ef4d" - ], - "26.0.0-linux_s390x": [ - "node-v26.0.0-linux-s390x.tar.xz", - "node-v26.0.0-linux-s390x", - "e3bd9df41f777dbb227b1261ea81b1fa9b654901bac8cace50a0b918b5160ab5" - ], - "26.0.0-linux_amd64": [ - "node-v26.0.0-linux-x64.tar.xz", - "node-v26.0.0-linux-x64", - "345d558514c62622b5c7d1f7b5f2a19c31ab1405d217df49f010c5ea8decc0f4" - ], - "26.0.0-windows_amd64": [ - "node-v26.0.0-win-x64.zip", - "node-v26.0.0-win-x64", - "d0418640a36096e00bddb57761aa0b1b98f91904ec4ed2b9dd75cbad723becd7" - ] - }, - "node_urls": [ - "https://nodejs.org/dist/v{version}/{filename}" - ], - "node_version": "26.0.0", - "include_headers": false, - "platform": "windows_arm64" - } - }, - "node26": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", - "attributes": { - "user_node_repository_name": "node26" - } - }, - "node26_host": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", - "attributes": { - "user_node_repository_name": "node26" - } - }, - "node26_toolchains": { - "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo", - "attributes": { - "user_node_repository_name": "node26" - } - } - }, - "recordedRepoMappingEntries": [] - } - }, - "@@rules_python+//python/extensions:pip.bzl%pip": { - "general": { - "bzlTransitiveDigest": "JOGVSMFw3XSY/6/iPEgXTZqFR5YFbDaa20WiXWZ8m4U=", - "usagesDigest": "AK1R124YPWwAs8z1CQYyjYuci8RO5Ofot+EP5ZCNQDc=", - "recordedFileInputs": { - "@@protobuf+//python/requirements.txt": "983be60d3cec4b319dcab6d48aeb3f5b2f7c3350f26b3a9e97486c37967c73c5", - "@@rules_fuzzing+//fuzzing/requirements.txt": "ab04664be026b632a0d2a2446c4f65982b7654f5b6851d2f9d399a19b7242a5b", - "@@rules_python+//tools/publish/requirements_darwin.txt": "2994136eab7e57b083c3de76faf46f70fad130bc8e7360a7fed2b288b69e79dc", - "@@rules_python+//tools/publish/requirements_linux.txt": "8175b4c8df50ae2f22d1706961884beeb54e7da27bd2447018314a175981997d", - "@@rules_python+//tools/publish/requirements_windows.txt": "7673adc71dc1a81d3661b90924d7a7c0fc998cd508b3cb4174337cef3f2de556" - }, - "recordedDirentsInputs": {}, - "envVariables": { - "RULES_PYTHON_REPO_DEBUG": null, - "RULES_PYTHON_REPO_DEBUG_VERBOSITY": null - }, - "generatedRepoSpecs": { - "pip_deps_310_numpy": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%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": { - "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%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": {} - } - } - }, - "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" - ] - ] - } - }, - "@@yq.bzl+//yq:extensions.bzl%yq": { - "general": { - "bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=", - "usagesDigest": "6JGNN6+d6JD7F98c3T4zg7eUxkfsfsMEDemA2CIoNQQ=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "yq_darwin_amd64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "darwin_amd64", - "version": "4.45.2" - } - }, - "yq_darwin_arm64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "darwin_arm64", - "version": "4.45.2" - } - }, - "yq_linux_amd64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "linux_amd64", - "version": "4.45.2" - } - }, - "yq_linux_arm64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "linux_arm64", - "version": "4.45.2" - } - }, - "yq_linux_s390x": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "linux_s390x", - "version": "4.45.2" - } - }, - "yq_linux_riscv64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "linux_riscv64", - "version": "4.45.2" - } - }, - "yq_linux_ppc64le": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "linux_ppc64le", - "version": "4.45.2" - } - }, - "yq_windows_amd64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "windows_amd64", - "version": "4.45.2" - } - }, - "yq_windows_arm64": { - "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", - "attributes": { - "platform": "windows_arm64", - "version": "4.45.2" + "platform": "windows_arm64", + "version": "4.45.2" } }, "yq_toolchains": { @@ -5421,5 +1251,9 @@ } } }, - "facts": {} + "facts": { + "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { + "notice_version": "1" + } + } } diff --git a/constants.bzl b/constants.bzl index abf02a051f90..143ee7243735 100644 --- a/constants.bzl +++ b/constants.bzl @@ -3,10 +3,10 @@ RELEASE_ENGINES_NODE = "^22.22.3 || ^24.15.0 || >=26.0.0" RELEASE_ENGINES_NPM = "^6.11.0 || ^7.5.6 || >=8.0.0" RELEASE_ENGINES_YARN = ">= 1.13.0" -NG_PACKAGR_VERSION = "^22.0.0-next.0" -ANGULAR_FW_VERSION = "^22.0.0-next.0" -ANGULAR_FW_PEER_DEP = "^22.0.0-next.0" -NG_PACKAGR_PEER_DEP = "^22.0.0-next.0" +NG_PACKAGR_VERSION = "^22.2.0-next.0" +ANGULAR_FW_VERSION = "^22.2.0-next.0" +ANGULAR_FW_PEER_DEP = "^22.0.0 || ^22.2.0-next.0" +NG_PACKAGR_PEER_DEP = "^22.0.0 || ^22.2.0-next.0" # Baseline widely-available date in `YYYY-MM-DD` format which defines Angular's # browser support. This date serves as the source of truth for the Angular CLI's diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index df3f8fe8c777..3c0b9bd67c53 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -219,7 +219,7 @@ export type NgPackagrBuilderOptions = { // @public export type UnitTestBuilderOptions = { browserViewport?: string; - browsers?: string[]; + browsers?: [string, ...string[]]; buildTarget?: string; coverage?: boolean; coverageExclude?: string[]; diff --git a/goldens/public-api/angular_devkit/build_webpack/index.api.md b/goldens/public-api/angular_devkit/build_webpack/index.api.md index abe0725a36fe..3e7ebf07873b 100644 --- a/goldens/public-api/angular_devkit/build_webpack/index.api.md +++ b/goldens/public-api/angular_devkit/build_webpack/index.api.md @@ -6,6 +6,7 @@ import { BuilderContext } from '@angular-devkit/architect'; import { BuilderOutput } from '@angular-devkit/architect'; +import type { Configuration } from 'webpack-dev-server'; import { Observable } from 'rxjs'; import type webpack from 'webpack'; import type WebpackDevServer from 'webpack-dev-server'; @@ -50,7 +51,7 @@ export function runWebpack(config: webpack.Configuration, context: BuilderContex // @public @deprecated (undocumented) export function runWebpackDevServer(config: webpack.Configuration, context: BuilderContext, options?: { shouldProvideStats?: boolean; - devServerConfig?: WebpackDevServer.Configuration; + devServerConfig?: Configuration; logging?: WebpackLoggingCallback; webpackFactory?: WebpackFactory; webpackDevServerFactory?: WebpackDevServerFactory; diff --git a/modules/testing/builder/package.json b/modules/testing/builder/package.json index 5f98e3097d25..f2db890d966d 100644 --- a/modules/testing/builder/package.json +++ b/modules/testing/builder/package.json @@ -4,12 +4,12 @@ "@angular-devkit/build-angular": "workspace:*", "@angular-devkit/core": "workspace:*", "@angular/ssr": "workspace:*", - "@vitest/coverage-v8": "4.1.8", + "@vitest/coverage-v8": "4.1.11", "browser-sync": "3.0.4", "istanbul-lib-instrument": "6.0.3", - "jsdom": "29.1.1", - "ng-packagr": "22.1.0-next.1", + "jsdom": "30.0.1", + "ng-packagr": "22.2.0-next.3", "rxjs": "7.8.2", - "vitest": "4.1.8" + "vitest": "4.1.11" } } diff --git a/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json b/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json index ac7b847f2366..c8105b2c16b4 100644 --- a/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json +++ b/modules/testing/builder/projects/hello-world-lib/projects/lib/package.json @@ -2,7 +2,7 @@ "name": "lib", "version": "0.0.1", "peerDependencies": { - "@angular/common": "^22.0.0-next", - "@angular/core": "^22.0.0-next" + "@angular/common": "^22.2.0-next", + "@angular/core": "^22.2.0-next" } } \ No newline at end of file diff --git a/package.json b/package.json index 6012a74835cb..967ecf1229c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "22.1.0-next.0", + "version": "22.2.0-next.4", "private": true, "description": "Software Development Kit for Angular", "keywords": [ @@ -28,12 +28,12 @@ "type": "git", "url": "git+https://github.com/angular/angular-cli.git" }, - "packageManager": "pnpm@11.5.2", + "packageManager": "pnpm@11.22.0", "engines": { "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "Please use pnpm instead of NPM to install dependencies", "yarn": "Please use pnpm instead of Yarn to install dependencies", - "pnpm": "11.5.2" + "pnpm": "11.22.0" }, "author": "Angular Authors", "license": "MIT", @@ -42,35 +42,35 @@ }, "homepage": "https://github.com/angular/angular-cli", "dependencies": { - "@angular/compiler-cli": "22.0.0", + "@angular/compiler-cli": "22.2.0-next.3", "typescript": "6.0.3" }, "devDependencies": { - "@angular/animations": "22.0.0", - "@angular/cdk": "22.0.0", - "@angular/common": "22.0.0", - "@angular/compiler": "22.0.0", - "@angular/core": "22.0.0", - "@angular/forms": "22.0.0", - "@angular/localize": "22.0.0", - "@angular/material": "22.0.0", - "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#74db100f1fee202d031c5f306778b847f577398c", - "@angular/platform-browser": "22.0.0", - "@angular/platform-server": "22.0.0", - "@angular/router": "22.0.0", - "@angular/service-worker": "22.0.0", - "@babel/core": "7.29.7", + "@angular/animations": "22.2.0-next.3", + "@angular/cdk": "22.2.0-next.2", + "@angular/common": "22.2.0-next.3", + "@angular/compiler": "22.2.0-next.3", + "@angular/core": "22.2.0-next.3", + "@angular/forms": "22.2.0-next.3", + "@angular/localize": "22.2.0-next.3", + "@angular/material": "22.2.0-next.2", + "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#bcad06e14cb29b52b0856020af5e2ab868075602", + "@angular/platform-browser": "22.2.0-next.3", + "@angular/platform-server": "22.2.0-next.3", + "@angular/router": "22.2.0-next.3", + "@angular/service-worker": "22.2.0-next.3", + "@babel/core": "8.0.1", "@bazel/bazelisk": "1.28.1", "@bazel/buildifier": "8.2.1", "@bazel/ibazel": "^0.28.0", "@eslint/compat": "2.1.0", - "@eslint/eslintrc": "3.3.5", + "@eslint/eslintrc": "3.3.6", "@eslint/js": "10.0.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.3", - "@rollup/wasm-node": "4.61.0", + "@rollup/wasm-node": "4.62.4", "@stylistic/eslint-plugin": "^5.0.0", "@tony.ganchev/eslint-plugin-header": "~3.4.0", "@types/babel__core": "7.20.5", @@ -78,7 +78,6 @@ "@types/browser-sync": "^2.27.0", "@types/express": "~5.0.1", "@types/http-proxy": "^1.17.4", - "@types/ini": "^4.0.0", "@types/jasmine": "~6.0.0", "@types/jasmine-reporters": "^2", "@types/karma": "^6.3.0", @@ -87,54 +86,51 @@ "@types/lodash": "^4.17.0", "@types/node": "^22.12.0", "@types/npm-package-arg": "^6.1.0", - "@types/pacote": "^11.1.3", "@types/picomatch": "^4.0.0", "@types/progress": "^2.0.3", "@types/semver": "^7.3.12", - "@types/watchpack": "^2.4.4", "@types/yargs": "^17.0.20", "@types/yargs-parser": "^21.0.0", - "@types/yarnpkg__lockfile": "^1.1.5", - "@typescript-eslint/eslint-plugin": "8.60.1", - "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", "ajv": "8.20.0", "buffer": "6.0.3", - "esbuild": "0.28.0", - "esbuild-wasm": "0.28.0", - "eslint": "10.4.1", + "esbuild": "0.28.2", + "esbuild-wasm": "0.28.2", + "eslint": "10.8.1", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", "express": "5.2.1", "fast-glob": "3.3.3", - "globals": "17.6.0", + "globals": "17.11.0", "http-proxy": "^1.18.1", - "http-proxy-middleware": "4.0.0", + "http-proxy-middleware": "4.2.0", "husky": "9.1.7", - "jasmine": "~6.2.0", - "jasmine-core": "~6.2.0", + "jasmine": "~6.3.0", + "jasmine-core": "~6.3.0", "jasmine-reporters": "^2.5.2", "jasmine-spec-reporter": "~7.0.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.2.0", + "karma-jasmine-html-reporter": "~2.3.0", "karma-source-map-support": "1.4.0", "lodash": "^4.17.21", - "magic-string": "0.30.21", + "magic-string": "1.2.1", "prettier": "^3.0.0", - "puppeteer": "25.1.0", - "quicktype-core": "23.2.6", - "rollup": "4.61.0", + "puppeteer": "25.8.0", + "quicktype-core": "26.0.0", + "rollup": "4.62.4", "rollup-license-plugin": "~3.2.0", - "rollup-plugin-dts": "6.4.1", - "rollup-plugin-sourcemaps2": "0.5.7", - "semver": "7.8.1", + "rollup-plugin-dts": "6.5.1", + "rollup-plugin-sourcemaps2": "0.5.8", + "semver": "7.8.5", "source-map-support": "0.5.21", "tslib": "2.8.1", - "undici": "8.3.0", + "undici": "8.10.0", "unenv": "^1.10.0", - "verdaccio": "6.7.2", + "verdaccio": "6.10.0", "verdaccio-auth-memory": "^13.0.0", "zone.js": "^0.16.0" }, diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 52eb43f9472c..75eedd6a7f5f 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -82,12 +82,13 @@ ts_project( ":node_modules/@angular-devkit/core", ":node_modules/@angular/ssr", ":node_modules/@babel/core", - ":node_modules/@babel/helper-annotate-as-pure", - ":node_modules/@babel/helper-split-export-declaration", ":node_modules/@inquirer/confirm", + ":node_modules/@oxc-project/types", + ":node_modules/@parcel/watcher", ":node_modules/@vitejs/plugin-basic-ssl", ":node_modules/beasties", ":node_modules/browserslist", + ":node_modules/chokidar", ":node_modules/https-proxy-agent", ":node_modules/istanbul-lib-instrument", ":node_modules/jsonc-parser", @@ -97,6 +98,7 @@ ts_project( ":node_modules/magic-string", ":node_modules/mrmime", ":node_modules/ng-packagr", + ":node_modules/oxc-parser", ":node_modules/parse5-html-rewriting-stream", ":node_modules/picomatch", ":node_modules/piscina", @@ -108,7 +110,7 @@ ts_project( ":node_modules/tinyglobby", ":node_modules/vite", ":node_modules/vitest", - ":node_modules/watchpack", + ":node_modules/xxhash-wasm", "//:node_modules/@angular/common", "//:node_modules/@angular/compiler", "//:node_modules/@angular/compiler-cli", @@ -123,7 +125,6 @@ ts_project( "//:node_modules/@types/node", "//:node_modules/@types/picomatch", "//:node_modules/@types/semver", - "//:node_modules/@types/watchpack", "//:node_modules/esbuild", "//:node_modules/esbuild-wasm", "//:node_modules/karma", @@ -144,8 +145,10 @@ ts_project( ":build", ":node_modules/@angular-devkit/core", ":node_modules/@babel/core", + ":node_modules/oxc-parser", "//:node_modules/@angular/compiler-cli", "//:node_modules/@types/jasmine", + "//:node_modules/esbuild", "//:node_modules/prettier", "//:node_modules/typescript", "//packages/angular/build/private", @@ -335,7 +338,7 @@ npm_package( ], stamp_files = [ "src/utils/version.js", - "src/tools/esbuild/utils.js", + "src/tools/esbuild/target.js", "src/utils/normalize-cache.js", "src/utils/supported-browsers.js", ], diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 2058061982ef..490f2f4b474c 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -20,44 +20,46 @@ "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER", - "@babel/core": "7.29.7", - "@babel/helper-annotate-as-pure": "7.29.7", - "@babel/helper-split-export-declaration": "7.24.7", + "@babel/core": "8.0.1", "@inquirer/confirm": "6.1.1", + "@parcel/watcher": "2.6.0", "@vitejs/plugin-basic-ssl": "2.3.0", - "beasties": "0.4.2", + "beasties": "0.4.3", "browserslist": "^4.26.0", - "esbuild": "0.28.0", - "https-proxy-agent": "9.0.0", + "chokidar": "5.0.0", + "esbuild": "0.28.2", + "https-proxy-agent": "9.1.0", "jsonc-parser": "3.3.1", - "listr2": "10.2.1", - "magic-string": "0.30.21", + "listr2": "11.0.0", + "magic-string": "1.2.1", "mrmime": "2.0.1", + "oxc-parser": "0.145.0", "parse5-html-rewriting-stream": "8.0.1", - "picomatch": "4.0.4", - "piscina": "5.1.4", - "rollup": "4.61.0", - "sass": "1.100.0", - "semver": "7.8.1", + "picomatch": "4.0.5", + "piscina": "5.3.0", + "rolldown": "1.2.4", + "sass": "1.102.0", + "semver": "7.8.5", "source-map-support": "0.5.21", "tinyglobby": "0.2.17", - "vite": "7.3.5", - "watchpack": "2.5.1" + "vite": "8.2.1", + "xxhash-wasm": "1.1.0" }, "optionalDependencies": { - "lmdb": "3.5.5" + "lmdb": "3.5.6" }, "devDependencies": { "@angular-devkit/core": "workspace:*", "@angular/ssr": "workspace:*", + "@oxc-project/types": "0.145.0", "istanbul-lib-instrument": "6.0.3", - "jsdom": "29.1.1", - "less": "4.6.4", - "ng-packagr": "22.1.0-next.1", - "postcss": "8.5.15", - "rolldown": "1.0.3", + "jsdom": "30.0.1", + "less": "4.9.0", + "ng-packagr": "22.2.0-next.3", + "postcss": "8.5.26", + "rollup": "4.62.4", "rxjs": "7.8.2", - "vitest": "4.1.8" + "vitest": "4.1.11" }, "peerDependencies": { "@angular/compiler": "0.0.0-ANGULAR-FW-PEER-DEP", @@ -73,6 +75,7 @@ "less": "^4.2.0", "ng-packagr": "0.0.0-NG-PACKAGR-PEER-DEP", "postcss": "^8.4.0", + "rollup": "^4.0.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", "typescript": ">=6.0 <6.1", @@ -112,6 +115,9 @@ "postcss": { "optional": true }, + "rollup": { + "optional": true + }, "tailwindcss": { "optional": true }, diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index 77d1f16cbd2a..e71663765274 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -15,6 +15,7 @@ import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-lan import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils'; import { ChangedFiles } from '../../tools/esbuild/watcher'; import { shouldWatchRoot } from '../../utils/environment-options'; +import { initializeHash } from '../../utils/hash'; import { NormalizedCachedOptions } from '../../utils/normalize-cache'; import { toPosixPath } from '../../utils/path'; import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; @@ -41,6 +42,7 @@ const packageWatchFiles = [ '.pnp.data.json', ]; +// eslint-disable-next-line max-lines-per-function export async function* runEsBuildBuildAction( action: (rebuildState?: RebuildState) => Promise, options: { @@ -78,10 +80,12 @@ export async function* runEsBuildBuildAction( incrementalResults, } = options; + await initializeHash(); + const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress; // Initial build - let result: ExecutionResult; + let result: ExecutionResult | undefined; try { // Perform the build action result = await withProgress('Building...', () => action()); @@ -89,8 +93,8 @@ export async function* runEsBuildBuildAction( // Log all diagnostic (error/warning/logs) messages await logMessages(logger, result, colors, jsonLogs); } finally { - // Ensure Sass workers are shutdown if not watching - if (!watch) { + // Ensure Sass workers are shutdown if not watching or if the initial build failed + if (!watch || !result) { shutdownSassWorkerPool(); } } @@ -104,20 +108,25 @@ export async function* runEsBuildBuildAction( logger.info('Watch mode enabled. Watching for file changes...'); } + const normalizedOutputBase = toPosixPath(outputOptions.base); + const normalizedCacheBase = toPosixPath(cacheOptions.basePath); const ignored: string[] = [ // Ignore the output and cache paths to avoid infinite rebuild cycles - outputOptions.base, - cacheOptions.basePath, + normalizedOutputBase, + `${normalizedOutputBase}/**`, + normalizedCacheBase, + `${normalizedCacheBase}/**`, `${toPosixPath(workspaceRoot)}/**/.*/**`, ]; // Setup a watcher const { createWatcher } = await import('../../tools/esbuild/watcher'); - watcher = createWatcher({ + watcher = await createWatcher({ polling: typeof poll === 'number', interval: poll, followSymlinks: preserveSymlinks, ignored, + cwd: workspaceRoot, }); // Setup abort support @@ -148,7 +157,13 @@ export async function* runEsBuildBuildAction( // Output the first build results after setting up the watcher to ensure that any code executed // higher in the iterator call stack will trigger the watcher. This is particularly relevant for // unit tests which execute the builder and modify the file system programmatically. - yield* emitOutputResults(result, outputOptions); + const outputResults = [...emitOutputResults(result, outputOptions)]; + if (!watch) { + await result.dispose(); + // Set to true to prevent double-disposal of the result context in the finally block on generator exit + watchLoopStarted = true; + } + yield* outputResults; // Finish if watch mode is not enabled if (!watcher) { @@ -209,6 +224,9 @@ export async function* runEsBuildBuildAction( // Remove any stale locations if the build was successful if (staleWatchFiles?.size) { watcher.remove([...staleWatchFiles]); + for (const staleFile of staleWatchFiles) { + currentWatchFiles.delete(staleFile); + } } for (const outputResult of emitOutputResults( diff --git a/packages/angular/build/src/builders/application/chunk-optimizer.ts b/packages/angular/build/src/builders/application/chunk-optimizer.ts index 1586b48155ea..ea4c2f8076f9 100644 --- a/packages/angular/build/src/builders/application/chunk-optimizer.ts +++ b/packages/angular/build/src/builders/application/chunk-optimizer.ts @@ -19,8 +19,8 @@ import type { Message, Metafile } from 'esbuild'; import assert from 'node:assert'; -import { type Plugin, rollup } from 'rollup'; -import { BundleContextResult } from '../../tools/esbuild/bundler-context'; +import type { Plugin } from 'rollup'; +import { BundleMergedContextResult } from '../../tools/esbuild/bundler-context'; import { type BuildOutputFile, BuildOutputFileType, @@ -212,9 +212,9 @@ function createChunkOptimizationFailureMessage(message: string): Message { */ // eslint-disable-next-line max-lines-per-function export async function optimizeChunks( - original: BundleContextResult, + original: BundleMergedContextResult, sourcemap: boolean | 'hidden', -): Promise { +): Promise { // Failed builds cannot be optimized if (original.errors) { return original; @@ -235,7 +235,7 @@ export async function optimizeChunks( } // No action required if no browser main entrypoint or metafile for stats - if (!mainFile || !original.metafile) { + if (!mainFile || !original.metafiles.browser) { return original; } @@ -302,10 +302,19 @@ export async function optimizeChunks( minify: { mangle: false, compress: false }, sourcemap, chunkFileNames: (chunkInfo) => - `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`, + `${chunkInfo.name.replace(/-[a-zA-Z0-9_-]{8}$/, '')}-[hash].js`, }); optimizedOutput = result.output; } else { + let rollup; + try { + rollup = (await import('rollup')).rollup; + } catch { + throw new Error( + `Rollup is required when 'NG_BUILD_CHUNKS_ROLLDOWN' is set to false. ` + + `Please install 'rollup' manually (e.g. 'npm install rollup --save-dev') to use this fallback.`, + ); + } bundle = await rollup({ input: mainFile, plugins: plugins as Plugin[], @@ -315,7 +324,7 @@ export async function optimizeChunks( compact: true, sourcemap, chunkFileNames: (chunkInfo) => - `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`, + `${chunkInfo.name.replace(/-[a-zA-Z0-9_-]{8}$/, '')}-[hash].js`, }); optimizedOutput = result.output; } @@ -331,9 +340,9 @@ export async function optimizeChunks( } // Update metafile - const newMetafile = bundleOutputToEsbuildMetafile(optimizedOutput, original.metafile); + const newMetafile = bundleOutputToEsbuildMetafile(optimizedOutput, original.metafiles.browser); // Add back the outputs that were not part of the optimization - for (const [path, output] of Object.entries(original.metafile.outputs)) { + for (const [path, output] of Object.entries(original.metafiles.browser.outputs)) { if (usedChunks.has(path)) { continue; } @@ -341,11 +350,11 @@ export async function optimizeChunks( newMetafile.outputs[path] = output; for (const inputPath of Object.keys(output.inputs)) { if (!newMetafile.inputs[inputPath]) { - newMetafile.inputs[inputPath] = original.metafile.inputs[inputPath]; + newMetafile.inputs[inputPath] = original.metafiles.browser.inputs[inputPath]; } } } - original.metafile = newMetafile; + original.metafiles.browser = newMetafile; // Remove used chunks and associated sourcemaps from the original result original.outputFiles = original.outputFiles.filter( @@ -409,6 +418,7 @@ export async function optimizeChunks( const record = createInitialFileRecord(entryRecord.depth + 1); + original.initialFiles.set(importPath, record); entriesToAnalyze.push([importPath, record]); } } diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index d751eb7d298e..8fe488a8ff76 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -8,18 +8,21 @@ import { BuilderContext } from '@angular-devkit/architect'; import { createAngularCompilation } from '../../tools/angular/compilation'; +import { AngularCompilationContext } from '../../tools/esbuild/angular/compilation-state'; import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; import { generateBudgetStats } from '../../tools/esbuild/budget-stats'; import { BundleContextResult, BundlerContext } from '../../tools/esbuild/bundler-context'; import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; import { BuildOutputFileType } from '../../tools/esbuild/bundler-files'; import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; +import { LOCALE_DATA_BASE_MODULE } from '../../tools/esbuild/i18n-locale-plugin'; import { extractLicenses } from '../../tools/esbuild/license-extractor'; import { profileAsync } from '../../tools/esbuild/profiling'; +import { transformSupportedBrowsersToTargets } from '../../tools/esbuild/target'; import { calculateEstimatedTransferSizes, + isZonelessApp, logBuildStats, - transformSupportedBrowsersToTargets, } from '../../tools/esbuild/utils'; import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator'; import { optimizeChunksThreshold } from '../../utils/environment-options'; @@ -34,6 +37,9 @@ import { inlineI18n, loadActiveTranslations } from './i18n'; import { NormalizedApplicationBuildOptions } from './options'; import { createComponentStyleBundler, setupBundlerContexts } from './setup-bundling'; +/** The esbuild error text prefix used to detect top-level await errors. */ +const TOP_LEVEL_AWAIT_ERROR_TEXT = + 'Top-level await is not available in the configured target environment'; // eslint-disable-next-line max-lines-per-function export async function executeBuild( options: NormalizedApplicationBuildOptions, @@ -69,285 +75,357 @@ export async function executeBuild( let bundlerContexts; let componentStyleBundler; let codeBundleCache; - let bundlingResult: BundleContextResult; + let bundlingIndividualResults: BundleContextResult[]; let templateUpdates: Map | undefined; - if (rebuildState) { - bundlerContexts = rebuildState.rebuildContexts; - componentStyleBundler = rebuildState.componentStyleBundler; - codeBundleCache = rebuildState.codeBundleCache; - templateUpdates = rebuildState.templateUpdates; - // Reset template updates for new rebuild - templateUpdates?.clear(); - - const allFileChanges = rebuildState.fileChanges.all; - - // Bundle all contexts that do not require TypeScript changed file checks. - // These will automatically use cached results based on the changed files. - bundlingResult = await BundlerContext.bundleAll(bundlerContexts.otherContexts, allFileChanges); - - // Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of - // all TypeScript related contexts. - const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges); - const typescriptResults: BundleContextResult[] = []; - for (const typescriptContext of bundlerContexts.typescriptContexts) { - typescriptContext.invalidate(allFileChanges); - const result = await typescriptContext.bundle(forceTypeScriptRebuild); - typescriptResults.push(result); - } - bundlingResult = BundlerContext.mergeResults([bundlingResult, ...typescriptResults]); - } else { - const target = transformSupportedBrowsersToTargets(browsers); - codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); - componentStyleBundler = createComponentStyleBundler(options, target); - if (options.templateUpdates) { - templateUpdates = new Map(); - } - bundlerContexts = setupBundlerContexts( - options, - target, - codeBundleCache, - componentStyleBundler, - // Create new reusable compilation for the appropriate mode based on the `jit` plugin option - await createAngularCompilation(!!options.jit, !options.serverEntryPoint), - templateUpdates, - ); - - // Bundle everything on initial build - bundlingResult = await BundlerContext.bundleAll([ - ...bundlerContexts.typescriptContexts, - ...bundlerContexts.otherContexts, - ]); - } + let angularCompilationContext: AngularCompilationContext | undefined; + let executionResult: ExecutionResult | undefined; + try { + if (rebuildState) { + bundlerContexts = rebuildState.rebuildContexts; + componentStyleBundler = rebuildState.componentStyleBundler; + codeBundleCache = rebuildState.codeBundleCache; + templateUpdates = rebuildState.templateUpdates; + executionResult = new ExecutionResult( + bundlerContexts, + componentStyleBundler, + codeBundleCache, + templateUpdates, + ); + // Reset template updates for new rebuild + templateUpdates?.clear(); - // Update any external component styles if enabled and rebuilding. - // TODO: Only attempt rebundling of invalidated styles once incremental build results are supported. - if (rebuildState && options.externalRuntimeStyles) { - componentStyleBundler.invalidate(rebuildState.fileChanges.all); + const allFileChanges = rebuildState.fileChanges.all; - const componentResults = await componentStyleBundler.bundleAllFiles(true, true); - bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]); - } + // Bundle all contexts that do not require TypeScript changed file checks. + // These will automatically use cached results based on the changed files. + bundlingIndividualResults = await BundlerContext.bundleAll( + bundlerContexts.otherContexts, + allFileChanges, + ); - const executionResult = new ExecutionResult( - bundlerContexts, - componentStyleBundler, - codeBundleCache, - templateUpdates, - ); - executionResult.addWarnings(bundlingResult.warnings); - - // Add used external component style referenced files to be watched - if (options.externalRuntimeStyles) { - executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles()); - } + // Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of + // all TypeScript related contexts. + const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges); + const typescriptResults: BundleContextResult[] = []; + for (const typescriptContext of bundlerContexts.typescriptContexts) { + typescriptContext.invalidate(allFileChanges); + const result = await typescriptContext.bundle(forceTypeScriptRebuild); + typescriptResults.push(result); + } + + bundlingIndividualResults.push(...typescriptResults); + } else { + const target = transformSupportedBrowsersToTargets(browsers); + codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); + componentStyleBundler = createComponentStyleBundler(options, target); + if (options.templateUpdates) { + templateUpdates = new Map(); + } + const angularCompilation = await createAngularCompilation( + !!options.jit, + !options.serverEntryPoint, + ); + angularCompilationContext = new AngularCompilationContext(angularCompilation); + bundlerContexts = setupBundlerContexts( + options, + target, + codeBundleCache, + componentStyleBundler, + angularCompilationContext, + templateUpdates, + ); + executionResult = new ExecutionResult( + bundlerContexts, + componentStyleBundler, + codeBundleCache, + templateUpdates, + ); - // Return if the bundling has errors - if (bundlingResult.errors) { - executionResult.addErrors(bundlingResult.errors); + // Bundle everything on initial build + bundlingIndividualResults = await BundlerContext.bundleAll([ + ...bundlerContexts.typescriptContexts, + ...bundlerContexts.otherContexts, + ]); + } - return executionResult; - } + // Update any external component styles if enabled and rebuilding. + // TODO: Only attempt rebundling of invalidated styles once incremental build results are supported. + if (rebuildState && options.externalRuntimeStyles) { + componentStyleBundler.invalidate(rebuildState.fileChanges.all); - // Optimize chunks if enabled and threshold is met. - // This pass uses Rollup/Rolldown to further optimize chunks generated by esbuild. - if (options.optimizationOptions.scripts) { - // Count lazy chunks (files not needed for initial load). - // Advanced chunk optimization is most beneficial when there are multiple lazy chunks. - const { metafile, initialFiles } = bundlingResult; - const lazyChunksCount = Object.keys(metafile.outputs).filter( - (path) => path.endsWith('.js') && !initialFiles.has(path), - ).length; - - // Only run if the number of lazy chunks meets the configured threshold. - // This avoids overhead for small projects with few chunks. - - // TODO: Remove this log once chunk optimization is supported for server builds as this - // causes the file to be renamed and thus causes incorrect preloading. - if (!options.serverEntryPoint && lazyChunksCount >= optimizeChunksThreshold) { - const { optimizeChunks } = await import('./chunk-optimizer'); - const optimizationResult = await profileAsync('OPTIMIZE_CHUNKS', () => - optimizeChunks( - bundlingResult, - options.sourcemapOptions.scripts ? !options.sourcemapOptions.hidden || 'hidden' : false, - ), - ); + const componentResults = await componentStyleBundler.bundleAllFiles(true, true); + bundlingIndividualResults.push(...componentResults); + } + + let bundlingResult = BundlerContext.mergeResults(bundlingIndividualResults); - if (optimizationResult.errors) { - executionResult.addErrors(optimizationResult.errors); + executionResult.addWarnings(bundlingResult.warnings); - return executionResult; + // Add used external component style referenced files to be watched + if (options.externalRuntimeStyles) { + executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles()); + } + + // In non-watch mode, the bundler rebuild contexts and Angular compilation + // are no longer needed once bundling completes. Dispose them early to free + // worker processes, TypeScript ASTs, and caches before post-bundling steps + // execute or before returning bundling errors. + // This is fire-and-forget so worker teardown runs concurrently in the background + // without blocking the critical path of post-bundle optimization steps. + // Any in-flight disposal is awaited in the builder action's finally block. + if (!options.watch) { + void angularCompilationContext?.dispose(); + void executionResult.dispose(); + } + + // Return if the bundling has errors + if (bundlingResult.errors) { + // If Zone.js is used, augment top-level await errors with a more helpful message. + // esbuild's default error mentions "target environment" with browser versions, but + // the actual reason is that async/await is downleveled for Zone.js compatibility. + if (!isZonelessApp(options.polyfills)) { + for (const error of bundlingResult.errors) { + if (error.text?.startsWith(TOP_LEVEL_AWAIT_ERROR_TEXT)) { + error.notes ??= []; + error.notes.push({ + text: + 'Top-level await is not supported in applications that use Zone.js. ' + + 'Consider removing Zone.js or moving this code into an async function. \n' + + 'For more information about zoneless Angular applications, visit: https://angular.dev/guide/zoneless', + location: null, + }); + } + } } - bundlingResult = optimizationResult; + executionResult.addErrors(bundlingResult.errors); + + return executionResult; } - } - // Analyze external imports if external options are enabled - if (options.externalPackages || bundlingResult.externalConfiguration) { - const { - externalConfiguration = [], - externalImports: { browser = [], server = [] }, - } = bundlingResult; - // Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*, - // which matches import paths like @foo/bar/baz. - // This means all paths within the @foo/bar package are also marked as external. - const exclusionsPrefixes = externalConfiguration.map((exclusion) => exclusion + '/'); - const exclusions = new Set(externalConfiguration); - const explicitExternal = new Set(); - - const isExplicitExternal = (dep: string): boolean => { - if (exclusions.has(dep)) { - return true; + // Optimize chunks if enabled and threshold is met. + // This pass uses Rollup/Rolldown to further optimize chunks generated by esbuild. + if (options.optimizationOptions.scripts) { + // Count lazy chunks (files not needed for initial load). + // Advanced chunk optimization is most beneficial when there are multiple lazy chunks. + const { metafiles, initialFiles } = bundlingResult; + const lazyChunksCount = Object.keys(metafiles.browser.outputs || {}).filter( + (path) => path.endsWith('.js') && !initialFiles.has(path), + ).length; + + // Only run if the number of lazy chunks meets the configured threshold. + // This avoids overhead for small projects with few chunks. + if (lazyChunksCount >= optimizeChunksThreshold) { + const { optimizeChunks } = await import('./chunk-optimizer'); + const optimizationResult = await profileAsync('OPTIMIZE_CHUNKS', () => + optimizeChunks( + bundlingResult, + options.sourcemapOptions.scripts ? !options.sourcemapOptions.hidden || 'hidden' : false, + ), + ); + + if (optimizationResult.errors) { + executionResult.addErrors(optimizationResult.errors); + + return executionResult; + } + + bundlingResult = optimizationResult; } + } - for (const prefix of exclusionsPrefixes) { - if (dep.startsWith(prefix)) { + // Analyze external imports if external options are enabled + if (options.externalPackages || bundlingResult.externalConfiguration) { + const { + externalConfiguration = [], + externalImports: { browser = [], server = [] }, + } = bundlingResult; + // Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*, + // which matches import paths like @foo/bar/baz. + // This means all paths within the @foo/bar package are also marked as external. + const exclusionsPrefixes = externalConfiguration.map((exclusion) => exclusion + '/'); + const exclusions = new Set(externalConfiguration); + const explicitExternal = new Set(); + + const isExplicitExternal = (dep: string): boolean => { + // Locale-data imports are injected by the builder itself (see i18n-locale-plugin) and must stay resolvable + // even when the containing package is externalized by the user. + if (dep.startsWith(`${LOCALE_DATA_BASE_MODULE}/`)) { + return false; + } + + if (exclusions.has(dep)) { return true; } - } - return false; - }; + for (const prefix of exclusionsPrefixes) { + if (dep.startsWith(prefix)) { + return true; + } + } + + return false; + }; + + const implicitBrowser: string[] = []; + for (const dep of browser) { + if (isExplicitExternal(dep)) { + explicitExternal.add(dep); + } else { + implicitBrowser.push(dep); + } + } - const implicitBrowser: string[] = []; - for (const dep of browser) { - if (isExplicitExternal(dep)) { - explicitExternal.add(dep); - } else { - implicitBrowser.push(dep); + const implicitServer: string[] = []; + for (const dep of server) { + if (isExplicitExternal(dep)) { + explicitExternal.add(dep); + } else { + implicitServer.push(dep); + } } + + executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]); } - const implicitServer: string[] = []; - for (const dep of server) { - if (isExplicitExternal(dep)) { - explicitExternal.add(dep); - } else { - implicitServer.push(dep); + const { + metafiles: { browser: browserMetafile, server: serverMetafile }, + initialFiles, + outputFiles, + } = bundlingResult; + const metafiles = [browserMetafile, serverMetafile]; + + executionResult.outputFiles.push(...outputFiles); + + // Analyze files for bundle budget failures if present + let budgetFailures: BudgetCalculatorResult[] | undefined; + if (options.budgets) { + const compatStats = generateBudgetStats(browserMetafile, outputFiles, initialFiles); + budgetFailures = [...checkBudgets(options.budgets, compatStats, true)]; + for (const { message, severity } of budgetFailures) { + if (severity === 'error') { + executionResult.addError(message); + } else { + executionResult.addWarning(message); + } } } - executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]); - } + // Calculate estimated transfer size if scripts are optimized + let estimatedTransferSizes; + if (optimizationOptions.scripts || optimizationOptions.styles.minify) { + estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles); + } - const { metafile, initialFiles, outputFiles } = bundlingResult; + // Check metafile for CommonJS module usage if optimizing scripts + if (optimizationOptions.scripts) { + const messages = checkCommonJSModules(browserMetafile, options.allowedCommonJsDependencies); + executionResult.addWarnings(messages); + } - executionResult.outputFiles.push(...outputFiles); + // Copy assets + if (assets) { + executionResult.addAssets(await resolveAssets(assets, workspaceRoot)); + } - // Analyze files for bundle budget failures if present - let budgetFailures: BudgetCalculatorResult[] | undefined; - if (options.budgets) { - const compatStats = generateBudgetStats(metafile, outputFiles, initialFiles); - budgetFailures = [...checkBudgets(options.budgets, compatStats, true)]; - for (const { message, severity } of budgetFailures) { - if (severity === 'error') { - executionResult.addError(message); - } else { - executionResult.addWarning(message); - } + // Extract and write licenses for used packages + if (options.extractLicenses) { + executionResult.addOutputFile( + '3rdpartylicenses.txt', + await extractLicenses(metafiles, workspaceRoot), + BuildOutputFileType.Root, + ); } - } - // Calculate estimated transfer size if scripts are optimized - let estimatedTransferSizes; - if (optimizationOptions.scripts || optimizationOptions.styles.minify) { - estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles); - } + // Watch input index HTML file if configured + if (options.indexHtmlOptions) { + executionResult.extraWatchFiles.push(options.indexHtmlOptions.input); + executionResult.htmlIndexPath = options.indexHtmlOptions.output; + executionResult.htmlBaseHref = options.baseHref; + } - // Check metafile for CommonJS module usage if optimizing scripts - if (optimizationOptions.scripts) { - const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies); - executionResult.addWarnings(messages); - } + // Create server app engine manifest + if (serverEntryPoint) { + executionResult.addOutputFile( + SERVER_APP_ENGINE_MANIFEST_FILENAME, + generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref), + BuildOutputFileType.ServerRoot, + ); + } - // Copy assets - if (assets) { - executionResult.addAssets(await resolveAssets(assets, workspaceRoot)); - } + // Perform i18n translation inlining if enabled + if (i18nOptions.shouldInline) { + const result = await inlineI18n(browserMetafile, options, executionResult, initialFiles); + executionResult.addErrors(result.errors); + executionResult.addWarnings(result.warnings); + executionResult.addPrerenderedRoutes(result.prerenderedRoutes); + } else { + const result = await executePostBundleSteps( + browserMetafile, + options, + executionResult.outputFiles, + executionResult.assetFiles, + initialFiles, + // Set lang attribute to the defined source locale if present + i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined, + ); - // Extract and write licenses for used packages - if (options.extractLicenses) { - executionResult.addOutputFile( - '3rdpartylicenses.txt', - await extractLicenses(metafile, workspaceRoot), - BuildOutputFileType.Root, - ); - } + // Deduplicate and add errors and warnings + executionResult.addErrors([...new Set(result.errors)]); + executionResult.addWarnings([...new Set(result.warnings)]); - // Watch input index HTML file if configured - if (options.indexHtmlOptions) { - executionResult.extraWatchFiles.push(options.indexHtmlOptions.input); - executionResult.htmlIndexPath = options.indexHtmlOptions.output; - executionResult.htmlBaseHref = options.baseHref; - } + executionResult.addPrerenderedRoutes(result.prerenderedRoutes); + executionResult.outputFiles.push(...result.additionalOutputFiles); + executionResult.assetFiles.push(...result.additionalAssets); + } - // Create server app engine manifest - if (serverEntryPoint) { - executionResult.addOutputFile( - SERVER_APP_ENGINE_MANIFEST_FILENAME, - generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref), - BuildOutputFileType.ServerRoot, - ); - } + if (serverEntryPoint) { + executionResult.addOutputFile( + 'prerendered-routes.json', + JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2), + BuildOutputFileType.Root, + ); + } - // Perform i18n translation inlining if enabled - if (i18nOptions.shouldInline) { - const result = await inlineI18n(metafile, options, executionResult, initialFiles); - executionResult.addErrors(result.errors); - executionResult.addWarnings(result.warnings); - executionResult.addPrerenderedRoutes(result.prerenderedRoutes); - } else { - const result = await executePostBundleSteps( - metafile, - options, - executionResult.outputFiles, - executionResult.assetFiles, - initialFiles, - // Set lang attribute to the defined source locale if present - i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined, - ); + // Write metafiles if stats option is enabled + if (options.stats) { + executionResult.addOutputFile( + 'browser-stats.json', + JSON.stringify(browserMetafile, null, 2), + BuildOutputFileType.Root, + ); - // Deduplicate and add errors and warnings - executionResult.addErrors([...new Set(result.errors)]); - executionResult.addWarnings([...new Set(result.warnings)]); + if (serverEntryPoint) { + executionResult.addOutputFile( + 'server-stats.json', + JSON.stringify(serverMetafile, null, 2), + BuildOutputFileType.Root, + ); + } + } - executionResult.addPrerenderedRoutes(result.prerenderedRoutes); - executionResult.outputFiles.push(...result.additionalOutputFiles); - executionResult.assetFiles.push(...result.additionalAssets); - } + if (!jsonLogs && !options.quiet) { + const changedFiles = + rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo); + executionResult.addLog( + logBuildStats( + metafiles, + outputFiles, + initialFiles, + budgetFailures, + colors, + changedFiles, + estimatedTransferSizes, + !!ssrOptions, + verbose, + ), + ); + } - executionResult.addOutputFile( - 'prerendered-routes.json', - JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2), - BuildOutputFileType.Root, - ); - - // Write metafile if stats option is enabled - if (options.stats) { - executionResult.addOutputFile( - 'stats.json', - JSON.stringify(metafile, null, 2), - BuildOutputFileType.Root, - ); - } + return executionResult; + } catch (error) { + await Promise.allSettled([angularCompilationContext?.dispose(), executionResult?.dispose()]); - if (!jsonLogs && !options.quiet) { - const changedFiles = - rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo); - executionResult.addLog( - logBuildStats( - metafile, - outputFiles, - initialFiles, - budgetFailures, - colors, - changedFiles, - estimatedTransferSizes, - !!ssrOptions, - verbose, - ), - ); + throw error; } - - return executionResult; } diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts index aa931aaa5206..cf76a14d9030 100644 --- a/packages/angular/build/src/builders/application/execute-post-bundle.ts +++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts @@ -30,12 +30,13 @@ import { } from '../../utils/server-rendering/models'; import { prerenderPages } from '../../utils/server-rendering/prerender'; import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; +import { injectDebugIds } from './inject-debug-ids'; import { INDEX_HTML_CSR, INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options'; import { OutputMode } from './schema'; /** * Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation. - * @param metafile An esbuild metafile object. + * @param browserMetafile An esbuild metafile object. * @param options The normalized application builder options used to create the build. * @param outputFiles The output files of an executed build. * @param assetFiles The assets of an executed build. @@ -44,7 +45,7 @@ import { OutputMode } from './schema'; */ // eslint-disable-next-line max-lines-per-function export async function executePostBundleSteps( - metafile: Metafile, + browserMetafile: Metafile, options: NormalizedApplicationBuildOptions, outputFiles: BuildOutputFile[], assetFiles: BuildOutputAsset[], @@ -79,6 +80,14 @@ export async function executePostBundleSteps( partialSSRBuild, } = options; + // Embed ECMA-426 Debug IDs into JS/source-map pairs before any consumer reads the bytes (in + // particular `generateIndexHtml` below, which computes SRI hashes from the on-disk content). + // Doing this here also covers the i18n path, where this function is invoked once per locale + // with locale-specific output files. Files without a source map sibling are skipped. + if (sourcemapOptions.scripts) { + injectDebugIds(outputFiles); + } + // Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR). // NOTE: Critical CSS inlining is deliberately omitted here, as it will be handled during server rendering. // Additionally, when using prerendering or AppShell, the index HTML file may be regenerated. @@ -122,7 +131,7 @@ export async function executePostBundleSteps( locale, baseHref, initialFilesPaths, - metafile, + browserMetafile, publicPath, ); @@ -205,7 +214,7 @@ export async function executePostBundleSteps( locale, baseHref, initialFilesPaths, - metafile, + browserMetafile, publicPath, ); diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index c83f1a29a30a..137336497885 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -8,6 +8,7 @@ import { BuilderContext } from '@angular-devkit/architect'; import type { Metafile } from 'esbuild'; +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { ExecutionResult, @@ -18,6 +19,7 @@ import { I18nInliner } from '../../tools/esbuild/i18n-inliner'; import { maxWorkers } from '../../utils/environment-options'; import { loadTranslations } from '../../utils/i18n-options'; import { createTranslationLoader } from '../../utils/load-translations'; +import { createProjectResolver } from '../../utils/resolve-project'; import { executePostBundleSteps } from './execute-post-bundle'; import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options'; @@ -48,6 +50,7 @@ export async function inlineI18n( outputFiles: executionResult.outputFiles, shouldOptimize: optimizationOptions.scripts, persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined, + localizeVersion: i18nOptions.localizeVersion, }, maxWorkers, ); @@ -72,10 +75,21 @@ export async function inlineI18n( try { for (const locale of i18nOptions.inlineLocales) { + const localeDescription = i18nOptions.locales[locale]; + let translationIntegrity: string | undefined = ''; + for (const file of localeDescription.files) { + if (!file.integrity) { + translationIntegrity = undefined; + break; + } + translationIntegrity += (translationIntegrity ? '|' : '') + file.integrity; + } + // A locale specific set of files is returned from the inliner. const localeInlineResult = await inliner.inlineForLocale( locale, - i18nOptions.locales[locale].translation, + localeDescription.translation, + translationIntegrity, ); const localeOutputFiles = localeInlineResult.outputFiles; inlineResult.errors.push(...localeInlineResult.errors); @@ -176,6 +190,15 @@ export async function loadActiveTranslations( context: BuilderContext, i18n: NormalizedApplicationBuildOptions['i18nOptions'], ) { + if (!i18n.localizeVersion) { + try { + const projectResolve = createProjectResolver(context.workspaceRoot); + const manifestPath = projectResolve('@angular/localize/package.json'); + const manifest = JSON.parse(await readFile(manifestPath, 'utf-8')) as { version?: string }; + i18n.localizeVersion = manifest.version; + } catch {} + } + // Load locale data and translations (if present) let loader; for (const [locale, desc] of Object.entries(i18n.locales)) { diff --git a/packages/angular/build/src/builders/application/inject-debug-ids.ts b/packages/angular/build/src/builders/application/inject-debug-ids.ts new file mode 100644 index 000000000000..59dc9c76f3af --- /dev/null +++ b/packages/angular/build/src/builders/application/inject-debug-ids.ts @@ -0,0 +1,52 @@ +/** + * @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 { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files'; +import { + generateDebugId, + injectDebugIdIntoJs, + injectDebugIdIntoSourceMap, + stripDebugIdFromSourceMap, +} from '../../utils/debug-id'; + +/** + * Embeds an ECMA-426 Debug ID into every browser JavaScript output that has a + * matching source map sibling. + * + * The Debug ID is derived deterministically (UUIDv5) from the source map bytes + * so rebuilds of the same source produce the same ID. The JS file gets a + * `//# debugId=` comment placed above any existing + * `//# sourceMappingURL=` line and the source map JSON gets a top-level + * `"debugId"` field. Together they make build artifacts self-identifying as + * proposed by https://github.com/tc39/ecma426/blob/main/proposals/debug-id.md. + */ +export function injectDebugIds(outputFiles: BuildOutputFile[]): void { + const filesByPath = new Map(); + for (const file of outputFiles) { + filesByPath.set(file.path, file); + } + + const encoder = new TextEncoder(); + + for (const file of outputFiles) { + if (file.type !== BuildOutputFileType.Browser || !file.path.endsWith('.js')) { + continue; + } + + const map = filesByPath.get(`${file.path}.map`); + if (!map) { + continue; + } + + const mapText = map.text; + const mapTextForHash = stripDebugIdFromSourceMap(mapText); + const id = generateDebugId(mapTextForHash); + file.contents = encoder.encode(injectDebugIdIntoJs(file.text, id)); + map.contents = encoder.encode(injectDebugIdIntoSourceMap(mapText, id)); + } +} diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 07d805d1cbf9..abe3298aa093 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -8,7 +8,6 @@ import type { BuilderContext } from '@angular-devkit/architect'; import type { Plugin } from 'esbuild'; -import { realpathSync } from 'node:fs'; import { access, constants, readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import path from 'node:path'; @@ -18,6 +17,7 @@ import { useJSONBuildLogs, usePartialSsrBuild } from '../../utils/environment-op import { I18nOptions, createI18nOptions } from '../../utils/i18n-options'; import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; +import { canonicalizePath } from '../../utils/path'; import { SearchDirectory, findTailwindConfiguration, @@ -25,6 +25,7 @@ import { loadPostcssConfiguration, } from '../../utils/postcss-configuration'; import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project-metadata'; +import { createProjectResolver } from '../../utils/resolve-project'; import { addTrailingSlash, joinUrlParts, stripLeadingSlash } from '../../utils/url'; import { Schema as ApplicationBuilderOptions, @@ -125,6 +126,20 @@ interface InternalOptions { * Suppress build summary and stats table. */ quiet?: boolean; + + /** + * Disables esbuild code splitting for the browser code bundle. + * + * Splitting emits shared chunks whose exports are read across chunk boundaries as live ESM + * bindings. A module hoisted into a shared chunk is wrapped in a lazy initializer, so its exported + * value is only assigned once that initializer runs. Runners that load the generated output + * through a module runner rather than the browser's own ESM implementation do not reliably + * preserve those bindings, and an importing chunk can observe the export as `undefined`. + * + * Test bundles are never downloaded by a browser, so there is nothing for splitting to optimize + * there. Used exclusively for tests and shouldn't be used for other kinds of builds. + */ + disableCodeSplitting?: boolean; } /** Full set of options for `application` builder. */ @@ -159,12 +174,7 @@ export async function normalizeOptions( options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'); // Setup base paths based on workspace root and project information - const workspaceRoot = preserveSymlinks - ? context.workspaceRoot - : // NOTE: promises.realpath should not be used here since it uses realpath.native which - // can cause case conversion and other undesirable behavior on Windows systems. - // ref: https://github.com/nodejs/node/issues/7726 - realpathSync(context.workspaceRoot); + const workspaceRoot = canonicalizePath(context.workspaceRoot); const projectMetadata = await context.getProjectMetadata(projectName); const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata); @@ -212,8 +222,7 @@ export async function normalizeOptions( } let loaderExtensions: - | Record - | undefined; + Record | undefined; if (options.loader) { for (const [extension, value] of Object.entries(options.loader)) { if (extension[0] !== '.' || /\.[cm]?[jt]sx?$/.test(extension)) { @@ -444,6 +453,7 @@ export async function normalizeOptions( partialSSRBuild = false, externalRuntimeStyles, instrumentForCoverage, + disableCodeSplitting, } = options; // Return all the normalized options @@ -480,6 +490,7 @@ export async function normalizeOptions( watch, workspaceRoot, entryPoints, + disableCodeSplitting, optimizationOptions, outputOptions, outExtension, @@ -728,9 +739,7 @@ function normalizeExternals(value: string[] | undefined): string[] | undefined { } async function findFrameworkVersion(projectRoot: string): Promise { - // Create a custom require function for ESM compliance. - // NOTE: The trailing slash is significant. - const projectResolve = createRequire(projectRoot + '/').resolve; + const projectResolve = createProjectResolver(projectRoot); try { const manifestPath = projectResolve('@angular/core/package.json'); diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts index 9b47bc67e49d..79209ba0eaf9 100644 --- a/packages/angular/build/src/builders/application/setup-bundling.ts +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import { AngularCompilation } from '../../tools/angular/compilation'; +import type { AngularCompilationContext } from '../../tools/esbuild/angular/compilation-state'; import { ComponentStylesheetBundler } from '../../tools/esbuild/angular/component-stylesheets'; -import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; +import type { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; import { createBrowserCodeBundleOptions, createBrowserPolyfillBundleOptions, @@ -19,8 +19,8 @@ import { import { BundlerContext } from '../../tools/esbuild/bundler-context'; import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts'; import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles'; -import { getSupportedNodeTargets } from '../../tools/esbuild/utils'; -import { NormalizedApplicationBuildOptions } from './options'; +import { getSupportedNodeTargets } from '../../tools/esbuild/target'; +import type { NormalizedApplicationBuildOptions } from './options'; /** * Generates one or more BundlerContext instances based on the builder provided @@ -35,7 +35,7 @@ export function setupBundlerContexts( target: string[], codeBundleCache: SourceFileCache, stylesheetBundler: ComponentStylesheetBundler, - angularCompilation: AngularCompilation, + angularCompilationContext: AngularCompilationContext, templateUpdates: Map | undefined, ): { typescriptContexts: BundlerContext[]; @@ -63,9 +63,10 @@ export function setupBundlerContexts( target, codeBundleCache, stylesheetBundler, - angularCompilation, + angularCompilationContext, templateUpdates, ), + true, ), ); @@ -75,12 +76,14 @@ export function setupBundlerContexts( target, codeBundleCache, stylesheetBundler, + angularCompilationContext.createSecondaryContext(), ); if (browserPolyfillBundleOptions) { const browserPolyfillContext = new BundlerContext( workspaceRoot, watch, browserPolyfillBundleOptions, + true, ); if (typeof browserPolyfillBundleOptions === 'function') { otherContexts.push(browserPolyfillContext); @@ -94,7 +97,9 @@ export function setupBundlerContexts( for (const initial of [true, false]) { const bundleOptions = createGlobalStylesBundleOptions(options, target, initial); if (bundleOptions) { - otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial)); + otherContexts.push( + new BundlerContext(workspaceRoot, watch, bundleOptions, true, () => initial), + ); } } } @@ -104,7 +109,9 @@ export function setupBundlerContexts( for (const initial of [true, false]) { const bundleOptions = createGlobalScriptsBundleOptions(options, target, initial); if (bundleOptions) { - otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial)); + otherContexts.push( + new BundlerContext(workspaceRoot, watch, bundleOptions, true, () => initial), + ); } } } @@ -117,7 +124,14 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, watch, - createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler), + createServerMainCodeBundleOptions( + options, + nodeTargets, + codeBundleCache, + stylesheetBundler, + angularCompilationContext.createSecondaryContext(), + ), + true, ), ); @@ -127,7 +141,14 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, watch, - createSsrEntryCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler), + createSsrEntryCodeBundleOptions( + options, + nodeTargets, + codeBundleCache, + stylesheetBundler, + angularCompilationContext.createSecondaryContext(), + ), + true, ), ); } @@ -140,7 +161,9 @@ export function setupBundlerContexts( ); if (serverPolyfillBundleOptions) { - otherContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions)); + otherContexts.push( + new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions, true), + ); } } diff --git a/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts new file mode 100644 index 000000000000..76ecbbc4329e --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts @@ -0,0 +1,187 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Fixture application with a server entry point and four lazy routes. + * Four lazy chunks exceed the default chunk optimization threshold (3), + * so the optimization pass runs without requiring the + * `NG_BUILD_OPTIMIZE_CHUNKS` environment variable (which is captured at + * module load time and cannot be toggled per spec). + * + * `shared.ts` is imported statically by both `main.ts` and two of the lazy + * components. esbuild emits such modules as a separate `chunk-*.js` shared + * chunk, while the chunk optimizer merges entry-reachable modules back into + * the main chunk. The absence of `chunk-*.js` files is therefore used as a + * signal that the optimization pass actually ran. + */ +const LAZY_ROUTE_NAMES = ['lazy-a', 'lazy-b', 'lazy-c', 'lazy-d'] as const; + +function lazyComponentSource(name: string, useShared: boolean): string { + const className = name.replace(/(^|-)(\w)/g, (_, __, c: string) => c.toUpperCase()); + + return ` + import { Component } from '@angular/core'; + ${useShared ? `import { sharedValue } from '../shared';` : ''} + + @Component({ + selector: 'app-${name}', + template: '

${name} works! ${useShared ? '{{ shared }}' : ''}

', + }) + export default class ${className}Component { + ${useShared ? `shared = sharedValue();` : ''} + } + `; +} + +const serverLazyRoutesFiles: Record = { + 'src/shared.ts': ` + export function sharedValue(): string { + return 'shared-' + Date.now().toString(36); + } + `, + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + + export const routes: Routes = [ + ${LAZY_ROUTE_NAMES.map( + (name) => `{ path: '${name}', loadComponent: () => import('./${name}.component') },`, + ).join('\n ')} + ]; + `, + ...Object.fromEntries( + LAZY_ROUTE_NAMES.map((name, index) => [ + `src/app/${name}.component.ts`, + lazyComponentSource(name, index < 2), + ]), + ), + 'src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { RouterOutlet } from '@angular/router'; + import { sharedValue } from '../shared'; + + @Component({ + selector: 'app-root', + imports: [RouterOutlet], + template: '

{{ shared }}

', + }) + export class AppComponent { + shared = sharedValue(); + } + `, + 'src/app/app.config.ts': ` + import { ApplicationConfig } from '@angular/core'; + import { provideRouter } from '@angular/router'; + import { routes } from './app.routes'; + + export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes)], + }; + `, + 'src/main.ts': ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app/app.component'; + import { appConfig } from './app/app.config'; + + bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); + `, + 'src/main.server.ts': ` + import { mergeApplicationConfig } from '@angular/core'; + import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; + import { provideServerRendering } from '@angular/platform-server'; + import { AppComponent } from './app/app.component'; + import { appConfig } from './app/app.config'; + + const serverConfig = mergeApplicationConfig(appConfig, { + providers: [provideServerRendering()], + }); + + const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, serverConfig, context); + + export default bootstrap; + `, +}; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Chunk optimization with a server entry point"', () => { + beforeEach(async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts'); + + return JSON.stringify(tsConfig); + }); + + await harness.writeFiles(serverLazyRoutesFiles); + }); + + it('generates a server manifest consistent with the optimized browser chunks', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: true, + polyfills: ['zone.js'], + optimization: true, + // Name lazy chunks after their route entry points so that only shared + // chunks use the `chunk-` prefix, which the assertions below rely on. + namedChunks: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // The chunk optimizer merges entry-reachable shared modules back into the + // main chunk. A remaining `chunk-*.js` shared chunk indicates the + // optimization pass did not run and this test would be vacuous. + expect(harness.hasFileMatch('dist/browser', /^chunk-/)).toBeFalse(); + + const manifestContent = harness.readFile('dist/server/angular-app-manifest.mjs'); + const mappingSource = /entryPointToBrowserMapping: (\{[\s\S]*?\n\})/.exec(manifestContent); + expect(mappingSource) + .withContext('entryPointToBrowserMapping should be present in the server manifest') + .not.toBeNull(); + + const mapping = JSON.parse(mappingSource![1]) as Record; + + // Every lazy route entry point must retain a mapping entry after optimization. + for (const name of LAZY_ROUTE_NAMES) { + const key = Object.keys(mapping).find((entryPoint) => + entryPoint.endsWith(`${name}.component.ts`), + ); + expect(key) + .withContext(`mapping entry for lazy route '${name}' should exist`) + .toBeDefined(); + } + + // Every browser file referenced by the mapping must exist on disk. + for (const files of Object.values(mapping)) { + for (const file of files) { + expect(harness.hasFile(`dist/browser/${file}`)) + .withContext(`mapped browser file '${file}' should exist`) + .toBeTrue(); + } + } + + // All scripts referenced by the index HTML must exist on disk. + const indexContent = harness.readFile('dist/browser/index.csr.html'); + const scriptRefs = [ + ...indexContent.matchAll(/<(?:script src|link rel="modulepreload" href)="([^"]+)"/g), + ].map((match) => match[1]); + expect(scriptRefs.length).toBeGreaterThan(0); + for (const file of scriptRefs) { + expect(harness.hasFile(`dist/browser/${file}`)) + .withContext(`index.html referenced file '${file}' should exist`) + .toBeTrue(); + } + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts index 22c4c32202bd..2de5c5f229ca 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -132,5 +132,119 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { { outputLogsOnFailure: false }, ); }); + + it('rebuilds PostCSS stylesheet after error on rebuild from plugin dependency', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.css'], + }); + + await harness.writeFile( + 'test-plugin.js', + ` + const fs = require('fs'); + const path = require('path'); + module.exports = () => { + return { + postcssPlugin: 'test-plugin', + Once(root, { result }) { + const themePath = path.join(path.dirname(root.source.input.file), 'theme.json'); + result.messages.push({ + type: 'dependency', + file: themePath, + }); + const data = fs.readFileSync(themePath, 'utf-8'); + const json = JSON.parse(data); + root.append('body { color: ' + json.color + '; }'); + }, + }; + }; + module.exports.postcss = true; + `, + ); + await harness.writeFile( + '.postcssrc.json', + JSON.stringify({ + plugins: { + './test-plugin.js': {}, + }, + }), + ); + await harness.writeFile('src/styles.css', '/* base */'); + await harness.writeFile('src/theme.json', '{"color": "aqua"}'); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + await harness.writeFile('src/theme.json', 'invalid-json'); + }, + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/theme.json', '{"color": "blue"}'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + it('rebuilds PostCSS stylesheet after CSS syntax error on initial build from import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.css'], + }); + + await harness.writeFile( + 'noop-plugin.js', + ` + module.exports = () => ({ postcssPlugin: 'noop-plugin' }); + module.exports.postcss = true; + `, + ); + await harness.writeFile( + '.postcssrc.json', + JSON.stringify({ + plugins: { + './noop-plugin.js': {}, + }, + }), + ); + await harness.writeFile('src/styles.css', "@import './a.css';"); + await harness.writeFile('src/a.css', "a { ' }"); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.css', 'body { color: aqua; }'); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + await harness.writeFile('src/a.css', 'body { color: blue; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/top-level-await-error_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/top-level-await-error_spec.ts new file mode 100644 index 000000000000..b0220529ee28 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/top-level-await-error_spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Top-level await error message"', () => { + it('should show a Zone.js-specific error when top-level await is used with Zone.js', async () => { + await harness.writeFile( + 'src/main.ts', + ` + // The export makes this file a module, which is required for top-level await. + export const value = await Promise.resolve('test'); + console.log(value); + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + 'Top-level await is not supported in applications that use Zone.js', + ), + }), + ); + }); + + it('should not show a Zone.js-specific error when top-level await is used without Zone.js', async () => { + await harness.writeFile( + 'src/main.ts', + ` + // The export makes this file a module, which is required for top-level await. + export const value = await Promise.resolve('test'); + console.log(value); + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: [], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining( + 'Top-level await is not supported in applications that use Zone.js', + ), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts b/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts index deb55e172109..bdb8f9428f56 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 @@ -74,5 +74,33 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { // If not externalized, build will fail with a Node.js platform builtin error expect(result?.success).toBeTrue(); }); + + it('should not externalize builder-injected i18n locale-data imports when @angular/common is external', async () => { + harness.useProject('test', { + root: '.', + sourceRoot: 'src', + cli: { + cache: { + enabled: false, + }, + }, + i18n: { + sourceLocale: 'fr', + }, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + externalDependencies: ['@angular/common'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/polyfills.js').toExist(); + harness + .expectFile('dist/browser/polyfills.js') + .content.not.toMatch(/['"]@angular\/common\/locales\/global\/fr['"]/); + }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts b/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts index 402200a27f9d..2c495e703a7b 100644 --- a/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/extract-licenses_spec.ts @@ -55,5 +55,96 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('MIT'); harness.expectFile('dist/browser/en-US/main.js').toExist(); }); + + it(`should extract license from a package with a lowercase 'license' file`, async () => { + await harness.writeFile( + 'node_modules/test-package-a/package.json', + JSON.stringify({ + name: 'test-package-a', + version: '1.0.0', + main: 'index.js', + license: 'MIT', + }), + ); + await harness.writeFile( + 'node_modules/test-package-a/index.js', + 'console.log("test-package-a");', + ); + await harness.writeFile('node_modules/test-package-a/license', 'TEST_LOWERCASE_LICENSE_TEXT'); + await harness.appendToFile('src/main.ts', "\nimport 'test-package-a';\n"); + + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness + .expectFile('dist/3rdpartylicenses.txt') + .content.toContain('TEST_LOWERCASE_LICENSE_TEXT'); + }); + + it(`should extract license from a package with an alternative license file name (e.g., 'MIT-LICENCE.txt')`, async () => { + await harness.writeFile( + 'node_modules/test-package-b/package.json', + JSON.stringify({ + name: 'test-package-b', + version: '1.0.0', + main: 'index.js', + license: 'MIT', + }), + ); + await harness.writeFile( + 'node_modules/test-package-b/index.js', + 'console.log("test-package-b");', + ); + await harness.writeFile( + 'node_modules/test-package-b/MIT-LICENCE.txt', + 'TEST_ALTERNATIVE_LICENSE_TEXT', + ); + await harness.appendToFile('src/main.ts', "\nimport 'test-package-b';\n"); + + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness + .expectFile('dist/3rdpartylicenses.txt') + .content.toContain('TEST_ALTERNATIVE_LICENSE_TEXT'); + }); + + it(`should extract license from a package with a custom license file specified in package.json`, async () => { + await harness.writeFile( + 'node_modules/test-package-c/package.json', + JSON.stringify({ + name: 'test-package-c', + version: '1.0.0', + main: 'index.js', + license: 'SEE LICENSE IN custom-license.md', + }), + ); + await harness.writeFile( + 'node_modules/test-package-c/index.js', + 'console.log("test-package-c");', + ); + await harness.writeFile( + 'node_modules/test-package-c/custom-license.md', + 'TEST_CUSTOM_LICENSE_TEXT', + ); + await harness.appendToFile('src/main.ts', "\nimport 'test-package-c';\n"); + + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('TEST_CUSTOM_LICENSE_TEXT'); + }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/options/index_spec.ts b/packages/angular/build/src/builders/application/tests/options/index_spec.ts index 11228658bbce..b53467caaec6 100644 --- a/packages/angular/build/src/builders/application/tests/options/index_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/index_spec.ts @@ -79,16 +79,17 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.expectFile('dist/browser/index.html').content.toContain('TEST_123'); }); - // TODO: Build needs to be fixed to not throw an unhandled exception for this case - xit('should fail build when a string path to non-existent file', async () => { + it('should fail build and terminate cleanly when a string path to non-existent file is provided', async () => { harness.useTarget('build', { ...BASE_OPTIONS, index: 'src/not-here.html', }); - const { result } = await harness.executeOnce({ outputLogsOnFailure: false }); + const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); - expect(result?.success).toBe(false); + expect(result).toBeUndefined(); + expect(error).toEqual(jasmine.any(Error)); + expect(error?.message).toMatch(/Failed to read index HTML file/i); harness.expectFile('dist/browser/index.html').toNotExist(); }); diff --git a/packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts b/packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts new file mode 100644 index 000000000000..f38ae4996d16 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Option: "statsJson"', () => { + it('generates only browser stats file containing valid metafile data when true', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + statsJson: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser-stats.json').toExist(); + harness.expectFile('dist/server-stats.json').toNotExist(); + + const browserStats = JSON.parse(harness.readFile('dist/browser-stats.json')); + expect(browserStats.inputs).toBeDefined(); + expect(browserStats.outputs).toBeDefined(); + expect(Object.keys(browserStats.outputs).length).toBeGreaterThan(0); + }); + + it('does not generate stats files when false', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + statsJson: false, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser-stats.json').toNotExist(); + harness.expectFile('dist/server-stats.json').toNotExist(); + }); + + it('does not generate stats files when not set', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser-stats.json').toNotExist(); + harness.expectFile('dist/server-stats.json').toNotExist(); + }); + + describe('server build', () => { + beforeEach(async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts'); + + return JSON.stringify(tsConfig); + }); + }); + + it('generates separated browser and server stats files for an SSR build', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: true, + statsJson: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser-stats.json').toExist(); + harness.expectFile('dist/server-stats.json').toExist(); + + const browserStats = JSON.parse(harness.readFile('dist/browser-stats.json')); + const serverStats = JSON.parse(harness.readFile('dist/server-stats.json')); + + const browserPaths = new Set(Object.keys(browserStats.outputs)); + const serverPaths = new Set(Object.keys(serverStats.outputs)); + + expect(serverPaths.size).toBeGreaterThan(0); + expect(browserPaths.size).toBeGreaterThan(0); + + for (const path of serverPaths) { + expect(browserPaths.has(path)) + .withContext(`Server output '${path}' should not appear in browser-stats.json`) + .toBeFalse(); + } + + for (const path of browserPaths) { + expect(serverPaths.has(path)) + .withContext(`Browser output '${path}' should not appear in server-stats.json`) + .toBeFalse(); + } + }); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts index 5153045dba73..ca9212b0d3e3 100644 --- a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts @@ -83,6 +83,83 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { expectNoLog(logs, /subresource-integrity/); }); + it(`embeds an ECMA-426 debugId in JS and source map and the integrity matches`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + subresourceIntegrity: true, + sourceMap: { scripts: true }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + const distDir = workspacePath('dist/browser'); + const allEntries = readdirSync(distDir); + const jsFiles = allEntries.filter( + (f) => f.endsWith('.js') && allEntries.includes(`${f}.map`), + ); + expect(jsFiles.length).toBeGreaterThan(0); + + const debugIdRe = /\/\/# debugId=([^\r\n]*)/; + const indexHtml = harness.readFile('dist/browser/index.html'); + const importmapMatch = indexHtml.match(/'), + }); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toBe( + 'export const greeting = "Bonjour \\"mon ami\\" \\\\ \' \\n ";\n', + ); + }); + + it('inlines translations containing placeholders', async () => { + const source = 'export const welcome = (name) => $localize`:@@welcome:Hello ${name}!`;\n'; + const { outputFiles, errors, warnings } = await createInliner([ + browserFile('main.js', source), + ]).inlineForLocale('fr', { + welcome: { + messageParts: ['Bonjour ', ' !'], + placeholderNames: ['PH'], + text: 'Bonjour {$PH} !', + }, + }); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toBe( + 'export const welcome = (name) => `Bonjour ${name} !`;\n', + ); + }); + + it('inlines multiple localize calls within the same file', async () => { + const source = + 'export const a = $localize`:@@greeting:Hello`;\nexport const b = $localize`:@@farewell:Goodbye`;\n'; + const { outputFiles, errors, warnings } = await createInliner([ + browserFile('main.js', source), + ]).inlineForLocale('fr', { + greeting: translationFor('Bonjour'), + farewell: translationFor('Au revoir'), + }); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toBe( + 'export const a = "Bonjour";\nexport const b = "Au revoir";\n', + ); + }); + + it('inlines translations across multiple files using multiple worker threads in parallel', async () => { + inliner = new I18nInliner( + { + missingTranslation: 'warning', + outputFiles: [ + browserFile('main.js', GREETING_SOURCE), + browserFile('chunk1.js', GREETING_SOURCE), + browserFile('chunk2.js', GREETING_SOURCE), + browserFile('chunk3.js', GREETING_SOURCE), + ], + }, + 4, + ); + + const { outputFiles, errors, warnings } = await inliner.inlineForLocale('fr', { + greeting: translationFor('Bonjour'), + }); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + for (const name of ['main.js', 'chunk1.js', 'chunk2.js', 'chunk3.js']) { + expect(findFile(outputFiles, name).text).toBe('export const greeting = "Bonjour";\n'); + } + }); + + it('leaves files without localize calls unmodified', async () => { + const { outputFiles } = await createInliner([ + browserFile('main.js', GREETING_SOURCE), + browserFile('other.js', 'export const answer = 42;\n'), + ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }); + + expect(findFile(outputFiles, 'other.js').text).toBe('export const answer = 42;\n'); + }); + + it('inlines nested $localize calls in post-order', async () => { + const source = + 'export const msg = $localize`:@@outer:You selected ${$localize`:@@inner:Apple`} for delivery.`;\n'; + const { outputFiles, errors, warnings } = await createInliner([ + browserFile('main.js', source), + ]).inlineForLocale('fr', { + inner: translationFor('Pomme'), + outer: { + messageParts: ['Vous avez sélectionné ', ' pour la livraison.'], + placeholderNames: ['PH'], + text: 'Vous avez sélectionné {$PH} pour la livraison.', + }, + }); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toBe( + 'export const msg = `Vous avez sélectionné ${"Pomme"} pour la livraison.`;\n', + ); + }); + + it('reports an error diagnostic when a $localize template has a malformed escape sequence', async () => { + const source = 'export const msg = $localize`:@@id:\\unicode:`;\n'; + const { errors } = await createInliner([browserFile('main.js', source)]).inlineForLocale( + 'fr', + {}, + ); + + expect(errors).toEqual([ + 'Malformed escape sequence in $localize template literal in file "main.js".', + ]); + }); + + it('inlines the translations of a locale when translationIntegrity is provided', async () => { + const { outputFiles, errors, warnings } = await createInliner([ + browserFile('main.js', GREETING_SOURCE), + ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }, 'sha256-test-integrity'); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toContain('"Bonjour"'); + expect(findFile(outputFiles, 'main.js').text).not.toContain('$localize'); + }); + + it('inlines the translations of a locale when localizeVersion is configured in options', async () => { + inliner = new I18nInliner( + { + missingTranslation: 'warning', + outputFiles: [browserFile('main.js', GREETING_SOURCE)], + localizeVersion: '20.2.0', + }, + 1, + ); + + const { outputFiles, errors, warnings } = await inliner.inlineForLocale( + 'fr', + { greeting: translationFor('Bonjour') }, + 'sha256-test-integrity', + ); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toContain('"Bonjour"'); + expect(findFile(outputFiles, 'main.js').text).not.toContain('$localize'); + }); +}); 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 ae94b62ca16d..603c65677b00 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts @@ -7,7 +7,7 @@ */ import type { Plugin, ResolveResult } from 'esbuild'; -import { createRequire } from 'node:module'; +import { createProjectResolver } from '../../utils/resolve-project'; /** * The internal namespace used by generated locale import statements and Angular locale data plugin. @@ -50,7 +50,7 @@ export function createAngularLocaleDataPlugin(): Plugin { } let exact = true; - let localeRequire: NodeJS.Require | undefined; + let projectResolve: ((packageName: string) => string) | 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,10 +73,10 @@ export function createAngularLocaleDataPlugin(): Plugin { let result: ResolveResult | undefined; const { packages, absWorkingDir } = build.initialOptions; if (packages === 'external' && absWorkingDir) { - localeRequire ??= createRequire(absWorkingDir + '/'); + projectResolve ??= createProjectResolver(absWorkingDir); try { - localeRequire.resolve(potentialPath); + projectResolve(potentialPath); result = { errors: [], @@ -94,6 +94,17 @@ export function createAngularLocaleDataPlugin(): Plugin { kind: 'import-statement', resolveDir: absWorkingDir, }); + if (result && result.external && absWorkingDir) { + projectResolve ??= createProjectResolver(absWorkingDir); + try { + const resolvedPath = projectResolve(potentialPath); + result = { + ...result, + external: false, + path: resolvedPath, + }; + } catch {} + } } if (result?.path) { 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 dfb7cfb2087f..f2ec7eccce6d 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts @@ -6,192 +6,247 @@ * found in the LICENSE file at https://angular.dev/license */ +import remapping, { type DecodedSourceMap, type EncodedSourceMap } from '@ampproject/remapping'; import { type PluginItem, transformAsync } from '@babel/core'; -import fs from 'node:fs'; import { createRequire } from 'node:module'; -import path from 'node:path'; +import { workerData } from 'node:worker_threads'; import Piscina from 'piscina'; +import { useBabelLinker } from '../../utils/environment-options.js'; +import { + findTrailingSourceMapComment, + isTrailingSourceMapComment, + loadInputSourceMap, + loadInputSourceMapFromUrl, + removeSourceMappingURL, +} from '../../utils/source-map'; +import { transform as transformWithOxc } from '../oxc/oxc-transform.js'; +import type { JavaScriptTransformerOptions } from './javascript-transformer'; interface JavaScriptTransformRequest { filename: string; data: string | Uint8Array; - sourcemap: boolean; - thirdPartySourcemaps: boolean; - advancedOptimizations: boolean; skipLinker?: boolean; sideEffects?: boolean; - jit: boolean; instrumentForCoverage?: boolean; } +interface TransformOptions extends Omit { + inputSourceMap?: EncodedSourceMap; + isAlreadyStripped?: boolean; +} + +const { + sourcemap = false, + thirdPartySourcemaps = false, + advancedOptimizations = false, + jit = false, +} = (workerData || {}) as Partial; + const textDecoder = new TextDecoder(); const textEncoder = new TextEncoder(); -/** - * The function name prefix for all Angular partial compilation functions. - * Used to determine if linking of a JavaScript file is required. - * If any additional declarations are added or otherwise changed in the linker, - * the names MUST begin with this prefix. - */ -const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare'; +async function instrumentCoverage( + filename: string, + data: string, + useInputSourcemap: boolean, +): Promise<{ code: string; map?: EncodedSourceMap }> { + try { + let resolvedPath = 'istanbul-lib-instrument'; + try { + const requireFn = createRequire(filename); + resolvedPath = requireFn.resolve('istanbul-lib-instrument'); + } catch { + // Fallback to pool worker import traversal + } + + const { createInstrumenter } = (await import( + resolvedPath + )) as typeof import('istanbul-lib-instrument'); + const instrumenter = createInstrumenter({ + produceSourceMap: useInputSourcemap, + esModules: true, + }); + + const inputSourceMap = useInputSourcemap ? loadInputSourceMap(filename, data) : undefined; + const instrumentedCode = instrumenter.instrumentSync( + data, + filename, + inputSourceMap as Parameters[2], + ); + const lastMap = useInputSourcemap + ? (instrumenter.lastSourceMap() as EncodedSourceMap) + : undefined; + + return { + code: instrumentedCode, + map: lastMap ?? undefined, + }; + } catch (error) { + throw new Error( + `The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`, + { cause: error }, + ); + } +} export default async function transformJavaScript( request: JavaScriptTransformRequest, ): Promise { const { filename, data, ...options } = request; - const textData = typeof data === 'string' ? data : textDecoder.decode(data); - const transformedData = await transformWithBabel(filename, textData, options); + const useInputSourcemap = + sourcemap && (!!thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); + + let textData: string; + let inputSourceMap: EncodedSourceMap | undefined; + let isAlreadyStripped = false; + + if (typeof data !== 'string') { + const trailing = findTrailingSourceMapComment(data); + if (trailing === null) { + // 0 comments: fast path, no sourcemap to load or strip + textData = textDecoder.decode(data); + isAlreadyStripped = true; + } else if (trailing !== undefined) { + if (useInputSourcemap) { + inputSourceMap = loadInputSourceMapFromUrl(filename, trailing.urlLine); + if (inputSourceMap !== undefined) { + // Valid trailing sourcemap comment confirmed: safe to slice code buffer for transformation passes. + // Note: If no passes modify the code, the untouched original `data` buffer is returned below. + textData = textDecoder.decode(trailing.code); + isAlreadyStripped = true; + } else { + // Not a valid trailing sourcemap (e.g. inside template literal): fallback to full decode + textData = textDecoder.decode(data); + } + } else if (isTrailingSourceMapComment(trailing.urlLine)) { + // Valid trailing sourcemap comment confirmed: safe to slice code buffer + textData = textDecoder.decode(trailing.code); + isAlreadyStripped = true; + } else { + // Fallback to full decode and state-machine stripping + textData = textDecoder.decode(data); + } + } else { + // Multiple comments or comment not at line start: fall back to full decode and string parser + textData = textDecoder.decode(data); + } + } else { + textData = data; + } + + const transformedData = await transformJavaScriptImpl(filename, textData, { + ...options, + inputSourceMap, + isAlreadyStripped, + }); + + // If no transformations modified the code, return the original untouched data buffer via `move`. + // This preserves any original trailing sourcemap comment and avoids re-encoding. + if (transformedData === textData && typeof data !== 'string') { + return Piscina.move(data); + } - // Transfer the data via `move` instead of cloning return Piscina.move(textEncoder.encode(transformedData)); } -/** - * Cached instance of the compiler-cli linker's createEs2015LinkerPlugin function. - */ -let linkerPluginCreator: - | typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin - | undefined; - -async function transformWithBabel( +async function transformJavaScriptImpl( filename: string, data: string, - options: Omit, + options: TransformOptions, ): Promise { - const shouldLink = !options.skipLinker && (await requiresLinking(filename, data)); + const shouldLink = !options.skipLinker; const useInputSourcemap = - options.sourcemap && - (!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); + sourcemap && (!!thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); - const plugins: PluginItem[] = []; + let code = data; + const maps: (DecodedSourceMap | EncodedSourceMap)[] = []; + let coverageMap: EncodedSourceMap | undefined; if (options.instrumentForCoverage) { - try { - let resolvedPath = 'istanbul-lib-instrument'; - try { - const requireFn = createRequire(filename); - resolvedPath = requireFn.resolve('istanbul-lib-instrument'); - } catch { - // Fallback to pool worker import traversal - } - - const istanbul = await import(resolvedPath); - const programVisitor = istanbul.programVisitor ?? istanbul.default?.programVisitor; - - if (!programVisitor) { - throw new Error('programVisitor is not available in istanbul-lib-instrument.'); - } - - const { default: coveragePluginFactory } = - await import('../babel/plugins/add-code-coverage.js'); - plugins.push(coveragePluginFactory(programVisitor)); - } catch (error) { - throw new Error( - `The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`, - { cause: error }, - ); - } + const result = await instrumentCoverage(filename, code, useInputSourcemap); + code = result.code; + coverageMap = result.map; } - if (shouldLink) { - // Lazy load the linker plugin only when linking is required - const linkerPlugin = await createLinkerPlugin(options); - plugins.push(linkerPlugin); + if (shouldLink && useBabelLinker) { + const { createEs2015LinkerPlugin } = await import('@angular/compiler-cli/linker/babel'); + const { ConsoleLogger, LogLevel } = await import('@angular/compiler-cli'); + + const result = await transformAsync(code, { + filename, + inputSourceMap: false, + sourceMaps: !!useInputSourcemap, + compact: false, + configFile: false, + babelrc: false, + browserslistConfigFile: false, + plugins: [ + createEs2015LinkerPlugin({ + fileSystem: { + exists: () => false, + readFile: () => '', + resolve: (...paths: string[]) => paths.join('/'), + dirname: (path: string) => path.split('/').slice(0, -1).join('/'), + relative: (_from: string, to: string) => to, + } as never, + logger: new ConsoleLogger(LogLevel.info), + linkerJitMode: jit, + // This is a workaround until https://github.com/angular/angular/issues/42769 is fixed. + sourceMapping: false, + }) as PluginItem, + ], + }); + + code = result?.code ?? code; + if (result?.map) { + maps.push(result.map as EncodedSourceMap); + } } - if (options.advancedOptimizations) { - const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } = - await import('../babel/plugins'); - + // Run Oxc linking and/or advanced optimizations in a single unified AST traversal pass + const oxcLink = shouldLink && !useBabelLinker; + if (oxcLink || advancedOptimizations) { const sideEffectFree = options.sideEffects === false; const safeAngularPackage = sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); - - plugins.push( - [markTopLevelPure, { topLevelSafeMode: !safeAngularPackage }], - elideAngularMetadata, - adjustTypeScriptEnums, - [adjustStaticMembers, { wrapDecorators: sideEffectFree }], - ); - } - - // If no additional transformations are needed, return the data directly - if (plugins.length === 0) { - // Strip sourcemaps if they should not be used - return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); + const topLevelSafeMode = !safeAngularPackage; + + const result = transformWithOxc(filename, code, { + link: oxcLink, + jit, + advancedOptimizations, + sourcemap: useInputSourcemap, + sideEffects: options.sideEffects, + topLevelSafeMode, + }); + code = result.code; + if (result.map) { + maps.push(result.map); + } } - const result = await transformAsync(data, { - filename, - inputSourceMap: (useInputSourcemap ? undefined : false) as undefined, - sourceMaps: useInputSourcemap ? 'inline' : false, - compact: false, - configFile: false, - babelrc: false, - browserslistConfigFile: false, - plugins, - }); - - const outputCode = result?.code ?? data; + if (useInputSourcemap) { + const baseMap = coverageMap ?? options.inputSourceMap ?? loadInputSourceMap(filename, data); + if (maps.length > 0 || coverageMap) { + if (!options.isAlreadyStripped) { + code = removeSourceMappingURL(code); + } + const remappingChain: (DecodedSourceMap | EncodedSourceMap)[] = maps.reverse(); + if (baseMap) { + remappingChain.push(baseMap); + } - // Strip sourcemaps if they should not be used. - // Babel will keep the original comments even if sourcemaps are disabled. - return useInputSourcemap - ? outputCode - : outputCode.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); -} + if (remappingChain.length > 0) { + const finalMap = remapping(remappingChain, () => null).toString(); + const base64Map = Buffer.from(finalMap).toString('base64'); + code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}`; + } + } -async function requiresLinking(path: string, source: string): Promise { - // @angular/core and @angular/compiler will cause false positives - // Also, TypeScript files do not require linking - if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) { - return false; + return code; } - // Check if the source code includes one of the declaration functions. - // There is a low chance of a false positive but the names are fairly unique - // and the result would be an unnecessary no-op additional plugin pass. - return source.includes(LINKER_DECLARATION_PREFIX); -} - -async function createLinkerPlugin(options: Omit) { - linkerPluginCreator ??= (await import('@angular/compiler-cli/linker/babel')) - .createEs2015LinkerPlugin; - - const linkerPlugin = linkerPluginCreator({ - linkerJitMode: options.jit, - // This is a workaround until https://github.com/angular/angular/issues/42769 is fixed. - sourceMapping: false, - logger: { - level: 1, // Info level - debug(...args: string[]) { - // eslint-disable-next-line no-console - console.debug(args); - }, - info(...args: string[]) { - // eslint-disable-next-line no-console - console.info(args); - }, - warn(...args: string[]) { - // eslint-disable-next-line no-console - console.warn(args); - }, - error(...args: string[]) { - // eslint-disable-next-line no-console - console.error(args); - }, - }, - fileSystem: { - resolve: path.resolve, - exists: fs.existsSync, - dirname: path.dirname, - relative: path.relative, - readFile: fs.readFileSync, - // Node.JS types don't overlap the Compiler types. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, - }); - - return linkerPlugin; + // Strip sourcemaps if they should not be used + return options.isAlreadyStripped ? code : removeSourceMappingURL(code); } diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts index b728a0f599e2..3ba0dfff45b1 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts @@ -6,12 +6,41 @@ * found in the LICENSE file at https://angular.dev/license */ -import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; +import { createContentHash } from '../../utils/hash'; import { IMPORT_EXEC_ARGV } from '../../utils/server-rendering/esm-in-memory-loader/utils'; +import { removeSourceMappingURL } from '../../utils/source-map'; import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool'; import { Cache } from './cache'; +const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare'; +const LINKER_DECLARATION_PREFIX_BYTES = Buffer.from(LINKER_DECLARATION_PREFIX, 'utf-8'); + +/** + * Determines whether JavaScript code requires Angular linker processing. + * + * @param path The full path to the file. + * @param data The data (string or Buffer) of the file. + * @returns True if the code contains an Angular partial declaration; otherwise false. + */ +function requiresLinking(path: string, data: string | Uint8Array): boolean { + // @angular/core and @angular/compiler will cause false positives + // Also, TypeScript files do not require linking + if (/[\\/]@angular[\\/](?:compiler|core)[\\/]|\.[cm]?tsx?$/.test(path)) { + return false; + } + + if (typeof data === 'string') { + return data.includes(LINKER_DECLARATION_PREFIX); + } + + const dataBuffer = Buffer.isBuffer(data) + ? data + : Buffer.from(data.buffer, data.byteOffset, data.byteLength); + + return dataBuffer.includes(LINKER_DECLARATION_PREFIX_BYTES); +} + /** * Transformation options that should apply to all transformed files and data. */ @@ -34,11 +63,22 @@ export class JavaScriptTransformer { #commonOptions: Required; #fileCacheKeyBase: Uint8Array; + /** Queue of pending transformation tasks waiting for an active concurrency slot. */ + #pendingTasks: { resolve: () => void; reject: (reason: Error) => void }[] = []; + + /** Current count of actively executing transformation tasks. */ + #activeTasks = 0; + + /** Maximum number of transformation tasks allowed to execute concurrently. */ + #maxConcurrent: number; + constructor( options: JavaScriptTransformerOptions, readonly maxThreads: number, private readonly cache?: Cache, ) { + // Maintain 2 active tasks per worker thread to keep transformation pipelines fully saturated + this.#maxConcurrent = Math.max(1, maxThreads * 2); // Extract options to ensure only the named options are serialized and sent to the worker const { sourcemap, @@ -53,6 +93,34 @@ export class JavaScriptTransformer { jit, }; this.#fileCacheKeyBase = Buffer.from(JSON.stringify(this.#commonOptions), 'utf-8'); + this.#workerPool = this.#ensureWorkerPool(); + } + + /** + * Executes a transformation action using a semaphore-based backpressure throttle. + * Prevents libuv thread pool saturation and excessive V8 heap accumulation. + * @param action A callback that produces a promise for the transformation result. + * @returns A promise resolving to the transformation result. + */ + async #runWithThrottle(action: () => Promise): Promise { + if (this.#activeTasks >= this.#maxConcurrent) { + await new Promise((resolve, reject) => { + this.#pendingTasks.push({ resolve, reject }); + }); + } else { + this.#activeTasks++; + } + + try { + return await action(); + } finally { + const next = this.#pendingTasks.shift(); + if (next) { + next.resolve(); + } else { + this.#activeTasks--; + } + } } #ensureWorkerPool(): WorkerPool { @@ -63,6 +131,8 @@ export class JavaScriptTransformer { const workerPoolOptions: WorkerPoolOptions = { filename: require.resolve('./javascript-transformer-worker'), maxThreads: this.maxThreads, + minThreads: this.maxThreads, + workerData: this.#commonOptions, }; // Prevent passing SSR `--import` (loader-hooks) from parent to child worker. @@ -90,46 +160,37 @@ export class JavaScriptTransformer { sideEffects?: boolean, instrumentForCoverage?: boolean, ): Promise { - const data = await readFile(filename); - - let result; - let cacheKey; - if (this.cache) { - // Create a cache key from the file data and options that effect the output. - // NOTE: If additional options are added, this may need to be updated. - // TODO: Consider xxhash or similar instead of SHA256 - const hash = createHash('sha256'); - hash.update(`${!!skipLinker}--${!!sideEffects}`); - hash.update(data); - hash.update(this.#fileCacheKeyBase); - cacheKey = hash.digest('hex'); + return this.#runWithThrottle(async () => { + const data = await readFile(filename); - try { - result = await this.cache?.get(cacheKey); - } catch { - // Failure to get the value should not fail the transform + let cacheKey: string | undefined; + if (this.cache) { + // Create a cache key from the file data and options that effect the output. + // NOTE: If additional options are added, this may need to be updated. + const hasher = createContentHash(); + hasher.update(`${!!skipLinker}--${!!sideEffects}`); + hasher.update(data); + hasher.update(this.#fileCacheKeyBase); + cacheKey = hasher.digest(); + + try { + const cached = await this.cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + } catch { + // Failure to get the value should not fail the transform + } } - } - if (result === undefined) { - // If there is no cache or no cached entry, process the file - result = (await this.#ensureWorkerPool().run( - { - filename, - data, - skipLinker, - sideEffects, - instrumentForCoverage, - ...this.#commonOptions, - }, - { - // 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 Uint8Array; - - // If there is a cache then store the result + const result = await this.transformData( + filename, + data, + !!skipLinker, + sideEffects, + instrumentForCoverage, + ); + if (this.cache && cacheKey) { try { await this.cache.put(cacheKey, result); @@ -137,9 +198,9 @@ export class JavaScriptTransformer { // Failure to store the value in the cache should not fail the transform } } - } - return result; + return result; + }); } /** @@ -153,32 +214,50 @@ export class JavaScriptTransformer { */ async transformData( filename: string, - data: string, + data: string | Uint8Array, skipLinker: boolean, sideEffects?: boolean, instrumentForCoverage?: boolean, ): Promise { + const shouldLink = !skipLinker && requiresLinking(filename, data); + // Perform a quick test to determine if the data needs any transformations. // This allows directly returning the data without the worker communication overhead. - if (skipLinker && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) { + if (!shouldLink && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) { const keepSourcemap = this.#commonOptions.sourcemap && (!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); - return Buffer.from( - keepSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''), - 'utf-8', - ); + if (typeof data === 'string') { + return Buffer.from(keepSourcemap ? data : removeSourceMappingURL(data), 'utf-8'); + } + + return keepSourcemap ? data : removeSourceMappingURL(data); } - return this.#ensureWorkerPool().run({ - filename, - data, - skipLinker, - sideEffects, - instrumentForCoverage, - ...this.#commonOptions, - }); + // Only standalone (non-pooled) ArrayBuffers can be transferred across worker threads. + // Node.js shares an internal 8KB ArrayBuffer pool for small buffers, and transferring + // a pooled buffer will throw a DataCloneError because detaching it invalidates other slices. + // In addition, SharedArrayBuffers cannot be transferred, and Yarn PnP has deserialization issues. + const isTransferable = + typeof data !== 'string' && + data.buffer instanceof ArrayBuffer && + data.byteOffset === 0 && + data.byteLength === data.buffer.byteLength && + !process.versions.pnp; + + return this.#ensureWorkerPool().run( + { + filename, + data, + skipLinker: !shouldLink, + sideEffects, + instrumentForCoverage, + }, + { + transferList: isTransferable ? [data.buffer] : undefined, + }, + ); } /** @@ -186,6 +265,12 @@ export class JavaScriptTransformer { * @returns A void promise that resolves when closing is complete. */ async close(): Promise { + const pending = this.#pendingTasks; + this.#pendingTasks = []; + for (const task of pending) { + task.reject(new Error('JavaScriptTransformer closed.')); + } + if (this.#workerPool) { try { await this.#workerPool.destroy(); diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts new file mode 100644 index 000000000000..b1cdeec07b44 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts @@ -0,0 +1,377 @@ +/** + * @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 { JavaScriptTransformer } from './javascript-transformer'; + +describe('JavaScriptTransformer sourcemaps', () => { + let transformer: JavaScriptTransformer; + + afterEach(async () => { + await transformer?.close(); + }); + + function extractSourcemap(code: string): Record | null { + const match = code.match( + /\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.+)/, + ); + if (!match) { + return null; + } + + return JSON.parse(Buffer.from(match[1], 'base64').toString('utf-8')); + } + + it('should remap correctly when only advanced optimizations are applied', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + advancedOptimizations: true, + }, + 1, + ); + + const inputMap = { + version: 3, + sources: ['src/app.ts'], + sourcesContent: ['const x = new SomeClass();'], + mappings: 'AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = `var x = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; + + const result = await transformer.transformData('src/app.js', input, true); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('src/app.ts'); + expect(typeof map?.['mappings']).toBe('string'); + expect((map?.['mappings'] as string).length).toBeGreaterThan(0); + }); + + it('should remap correctly when only linking is applied', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + thirdPartySourcemaps: true, + }, + 1, + ); + + const inputMap = { + version: 3, + sources: ['node_modules/my-lib/directive.ts'], + sourcesContent: ['export class MyDirective {}'], + mappings: 'AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = ` + import * as i0 from "@angular/core"; + export class MyDirective {} + MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyDirective, + selector: "[my-dir]" + }); + //# sourceMappingURL=data:application/json;base64,${base64Map} + `; + + const result = await transformer.transformData( + 'node_modules/my-lib/directive.js', + input, + false, + ); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('node_modules/my-lib/directive.ts'); + expect(typeof map?.['mappings']).toBe('string'); + expect((map?.['mappings'] as string).length).toBeGreaterThan(0); + }); + + it('should defer and chain remapping when both linking and advanced optimizations are applied', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + thirdPartySourcemaps: true, + advancedOptimizations: true, + }, + 1, + ); + + const inputMap = { + version: 3, + sources: ['node_modules/my-lib/component.ts'], + sourcesContent: ['export class MyComponent {}'], + mappings: 'AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = ` + import * as i0 from "@angular/core"; + export class MyComponent {} + MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyComponent, + selector: "my-cmp", + template: "
" + }); + //# sourceMappingURL=data:application/json;base64,${base64Map} + `; + + const result = await transformer.transformData( + 'node_modules/my-lib/component.js', + input, + false, + ); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('node_modules/my-lib/component.ts'); + expect(typeof map?.['mappings']).toBe('string'); + expect((map?.['mappings'] as string).length).toBeGreaterThan(0); + }); + + it('should produce a valid sourcemap when no input sourcemap is present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + advancedOptimizations: true, + }, + 1, + ); + + const input = 'var x = new SomeClass();'; + const result = await transformer.transformData('src/app.js', input, true); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('src/app.js'); + expect(typeof map?.['mappings']).toBe('string'); + expect((map?.['mappings'] as string).length).toBeGreaterThan(0); + }); + + it('should remap correctly when coverage instrumentation is applied with an input sourcemap', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + }, + 1, + ); + + const inputMap = { + version: 3, + sources: ['src/counter.ts'], + sourcesContent: ['export function add(a: number, b: number) { return a + b; }'], + mappings: 'AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = `export function add(a, b) { return a + b; }\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; + + const result = await transformer.transformData( + 'src/counter.js', + input, + true, + undefined, + true /* instrumentForCoverage */, + ); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('src/counter.ts'); + expect(typeof map?.['mappings']).toBe('string'); + expect((map?.['mappings'] as string).length).toBeGreaterThan(0); + }); + + it('should defer and chain remapping when coverage instrumentation and advanced optimizations are applied', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + advancedOptimizations: true, + }, + 1, + ); + + const inputMap = { + version: 3, + sources: ['src/app.ts'], + sourcesContent: ['const x = new SomeClass();'], + mappings: 'AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = `var x = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; + + const result = await transformer.transformData( + 'src/app.js', + input, + true, + undefined, + true /* instrumentForCoverage */, + ); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('src/app.ts'); + expect(typeof map?.['mappings']).toBe('string'); + expect((map?.['mappings'] as string).length).toBeGreaterThan(0); + }); + + it('should accept a Uint8Array input in transformData', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: true, + advancedOptimizations: true, + }, + 1, + ); + + const inputBuffer = Buffer.from('var x = new SomeClass();', 'utf-8'); + const result = await transformer.transformData('src/app.js', inputBuffer, true); + const text = Buffer.from(result).toString('utf-8'); + const map = extractSourcemap(text); + + expect(map).toBeDefined(); + expect(map?.['version']).toBe(3); + expect(map?.['sources']).toContain('src/app.js'); + expect(typeof map?.['mappings']).toBe('string'); + }); + + it('should strip trailing sourcemap comments from Uint8Array input on fast-path', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + }, + 1, + ); + + const inputBuffer = Buffer.from( + 'console.log("hello");\n//# sourceMappingURL=app.js.map', + 'utf-8', + ); + const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, true); + const text = Buffer.from(result).toString('utf-8'); + + expect(text).toBe('console.log("hello");\n'); + }); + + it('should return Uint8Array input untouched on fast-path when no sourcemap comment is present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + }, + 1, + ); + + const inputBuffer = Buffer.from('console.log("hello");\nconst x = 1;', 'utf-8'); + const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, true); + + expect(result).toBe(inputBuffer); + }); + + it('should return Uint8Array untouched when skipLinker is false but file contains no linker declarations', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + }, + 1, + ); + + const inputBuffer = Buffer.from('console.log("no linking required");\nconst x = 1;', 'utf-8'); + const result = await transformer.transformData( + 'node_modules/my-lib/lib.js', + inputBuffer, + false, // skipLinker: false + ); + + expect(result).toBe(inputBuffer); + }); + + it('should bypass worker and skip linking for @angular/core and @angular/compiler paths', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + }, + 1, + ); + + const inputBuffer = Buffer.from('export const ɵɵngDeclareDirective = () => {};', 'utf-8'); + const result = await transformer.transformData( + 'node_modules/@angular/core/fesm2022/core.mjs', + inputBuffer, + false, + ); + + expect(result).toBe(inputBuffer); + }); + + it('should bypass worker and skip linking for TypeScript file extensions (.ts, .tsx, .mts, .cts)', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + }, + 1, + ); + + const inputBuffer = Buffer.from('export const ɵɵngDeclareDirective = () => {};', 'utf-8'); + + for (const ext of ['.ts', '.tsx', '.mts', '.cts']) { + const result = await transformer.transformData(`src/app/directive${ext}`, inputBuffer, false); + + expect(result).toBe(inputBuffer); + } + }); + + it('should not exclude packages with similar prefixes such as @angular/compiler-cli', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + }, + 1, + ); + + const input = ` + import * as i0 from "@angular/core"; + export class MyDirective {} + MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyDirective, + selector: "[my-dir]" + }); + `; + + const result = await transformer.transformData( + 'node_modules/@angular/compiler-cli/test.js', + input, + false, + ); + const text = Buffer.from(result).toString('utf-8'); + + expect(text).not.toContain('i0.ɵɵngDeclareDirective'); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/license-extractor.ts b/packages/angular/build/src/tools/esbuild/license-extractor.ts index 6629bbc387bf..4c73012033bb 100644 --- a/packages/angular/build/src/tools/esbuild/license-extractor.ts +++ b/packages/angular/build/src/tools/esbuild/license-extractor.ts @@ -7,7 +7,7 @@ */ import type { Metafile } from 'esbuild'; -import { readFile } from 'node:fs/promises'; +import { readFile, readdir } from 'node:fs/promises'; import path from 'node:path'; /** @@ -30,9 +30,9 @@ const NODE_MODULE_SEGMENT = 'node_modules'; const CUSTOM_LICENSE_TEXT = 'SEE LICENSE IN '; /** - * A list of commonly named license files found within packages. + * A regular expression for commonly named license files found within packages. */ -const LICENSE_FILES = ['LICENSE', 'LICENSE.txt', 'LICENSE.md']; +const LICENSE_FILE_REGEXP = /^(?:mit-)?licen[cs]e(?:$|[-._])/i; /** * Header text that will be added to the top of the output license extraction file. @@ -60,115 +60,132 @@ const EXTRACTION_FILE_SEPARATOR = '-'.repeat(80) + '\n'; * @param rootDirectory The root directory of the workspace. * @returns A string containing the content of the output licenses file. */ -export async function extractLicenses(metafile: Metafile, rootDirectory: string) { +export async function extractLicenses( + metafiles: Metafile[], + rootDirectory: string, +): Promise { let extractedLicenseContent = `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}`; const seenPaths = new Set(); + const seenPackageDirectories = new Set(); const seenPackages = new Set(); - for (const entry of Object.values(metafile.outputs)) { - for (const [inputPath, { bytesInOutput }] of Object.entries(entry.inputs)) { - // Skip if not included in output - if (bytesInOutput <= 0) { - continue; - } + for (const metafile of metafiles) { + for (const entry of Object.values(metafile.outputs)) { + for (const [inputPath, { bytesInOutput }] of Object.entries(entry.inputs)) { + // Skip if not included in output + if (bytesInOutput <= 0) { + continue; + } - // Skip already processed paths - if (seenPaths.has(inputPath)) { - continue; - } - seenPaths.add(inputPath); + // Skip already processed paths + if (seenPaths.has(inputPath)) { + continue; + } + seenPaths.add(inputPath); - // Skip non-package paths - if (!inputPath.includes(NODE_MODULE_SEGMENT)) { - continue; - } + // Skip non-package paths + if (!inputPath.includes(NODE_MODULE_SEGMENT)) { + continue; + } - // Extract the package name from the path - let baseDirectory = path.join(rootDirectory, inputPath); - let nameOrScope, nameOrFile; - let found = false; - while (baseDirectory !== path.dirname(baseDirectory)) { - const segment = path.basename(baseDirectory); - if (segment === NODE_MODULE_SEGMENT) { - found = true; - break; + // Extract the package name from the path + let baseDirectory = path.join(rootDirectory, inputPath); + let nameOrScope, nameOrFile; + let found = false; + while (baseDirectory !== path.dirname(baseDirectory)) { + const segment = path.basename(baseDirectory); + if (segment === NODE_MODULE_SEGMENT) { + found = true; + break; + } + + nameOrFile = nameOrScope; + nameOrScope = segment; + baseDirectory = path.dirname(baseDirectory); } - nameOrFile = nameOrScope; - nameOrScope = segment; - baseDirectory = path.dirname(baseDirectory); - } + // Skip non-package path edge cases that are not caught in the includes check above + if (!found || !nameOrScope) { + continue; + } - // Skip non-package path edge cases that are not caught in the includes check above - if (!found || !nameOrScope) { - continue; - } + const packageName = nameOrScope.startsWith('@') + ? `${nameOrScope}/${nameOrFile}` + : nameOrScope; + const packageDirectory = path.join(baseDirectory, packageName); - const packageName = nameOrScope.startsWith('@') - ? `${nameOrScope}/${nameOrFile}` - : nameOrScope; - const packageDirectory = path.join(baseDirectory, packageName); - - // Load the package's metadata to find the package's name, version, and license type - const packageJsonPath = path.join(packageDirectory, 'package.json'); - let packageJson; - try { - packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as { - name: string; - version: string; - // The object form is deprecated and should only be present in old packages - license?: string | { type: string }; - }; - } catch { - // Invalid package - continue; - } + if (seenPackageDirectories.has(packageDirectory)) { + continue; + } + seenPackageDirectories.add(packageDirectory); + + // Load the package's metadata to find the package's name, version, and license type + const packageJsonPath = path.join(packageDirectory, 'package.json'); + let packageJson; + try { + packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as { + name: string; + version: string; + // The object form is deprecated and should only be present in old packages + license?: string | { type: string }; + }; + } catch { + // Invalid package + continue; + } - // Skip already processed packages - const packageId = `${packageName}@${packageJson.version}`; - if (seenPackages.has(packageId)) { - continue; - } - seenPackages.add(packageId); - - // Attempt to find license text inside package - let licenseText = ''; - if ( - typeof packageJson.license === 'string' && - packageJson.license.toLowerCase().startsWith(CUSTOM_LICENSE_TEXT) - ) { - // Attempt to load the package's custom license - let customLicensePath; - const customLicenseFile = path.normalize( - packageJson.license.slice(CUSTOM_LICENSE_TEXT.length + 1).trim(), - ); - if (customLicenseFile.startsWith('..') || path.isAbsolute(customLicenseFile)) { - // Path is attempting to access files outside of the package - // TODO: Issue warning? - } else { - customLicensePath = path.join(packageDirectory, customLicenseFile); - try { - licenseText = await readFile(customLicensePath, 'utf-8'); - break; - } catch {} + // Skip already processed packages + const packageId = `${packageName}@${packageJson.version}`; + if (seenPackages.has(packageId)) { + continue; } - } else { - // Search for a license file within the root of the package - for (const potentialLicense of LICENSE_FILES) { - const packageLicensePath = path.join(packageDirectory, potentialLicense); - try { - licenseText = await readFile(packageLicensePath, 'utf-8'); - break; - } catch {} + seenPackages.add(packageId); + + // Attempt to find license text inside package + let licenseText = ''; + if ( + typeof packageJson.license === 'string' && + packageJson.license.toUpperCase().startsWith(CUSTOM_LICENSE_TEXT) + ) { + // Attempt to load the package's custom license + let customLicensePath; + const customLicenseFile = path.normalize( + packageJson.license.slice(CUSTOM_LICENSE_TEXT.length).trim(), + ); + if (customLicenseFile.startsWith('..') || path.isAbsolute(customLicenseFile)) { + // Path is attempting to access files outside of the package + // TODO: Issue warning? + } else { + customLicensePath = path.join(packageDirectory, customLicenseFile); + try { + licenseText = await readFile(customLicensePath, 'utf-8'); + } catch {} + } + } else { + // Search for a license file within the root of the package + const entries = await readdir(packageDirectory, { withFileTypes: true }).catch(() => []); + + for (const entry of entries) { + if ( + (entry.isFile() || entry.isSymbolicLink()) && + LICENSE_FILE_REGEXP.test(entry.name) + ) { + const packageLicensePath = path.join(packageDirectory, entry.name); + try { + licenseText = await readFile(packageLicensePath, 'utf-8'); + break; + } catch {} + } + } } - } - // Generate the package's license entry in the output content - extractedLicenseContent += `Package: ${packageJson.name}\n`; - extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, null, 2)}\n`; - extractedLicenseContent += `\n${licenseText}\n`; - extractedLicenseContent += EXTRACTION_FILE_SEPARATOR; + // Generate the package's license entry in the output content + extractedLicenseContent += `Package: ${packageJson.name}\n`; + extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, null, 2)}\n`; + extractedLicenseContent += `\n${licenseText}\n`; + extractedLicenseContent += EXTRACTION_FILE_SEPARATOR; + } } } diff --git a/packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts b/packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts index dba108285342..d8f95b8de9da 100644 --- a/packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts +++ b/packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts @@ -7,9 +7,9 @@ */ import { RootDatabase, open } from 'lmdb'; -import { Cache, CacheStore } from './cache'; +import { Cache, PersistentCacheStore } from './cache'; -export class LmdbCacheStore implements CacheStore { +export class LmdbCacheStore implements PersistentCacheStore { readonly #cacheFileUrl; #db: RootDatabase | undefined; diff --git a/packages/angular/build/src/tools/esbuild/load-result-cache.ts b/packages/angular/build/src/tools/esbuild/load-result-cache.ts index 30067486a384..00f803f2fdc7 100644 --- a/packages/angular/build/src/tools/esbuild/load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/load-result-cache.ts @@ -10,7 +10,7 @@ import type { OnLoadResult, PluginBuild } from 'esbuild'; import { normalize } from 'node:path'; export interface LoadResultCache { - get(path: string): OnLoadResult | undefined; + get(path: string): OnLoadResult | Promise | undefined; put(path: string, result: OnLoadResult): Promise; readonly watchFiles: ReadonlyArray; } @@ -25,7 +25,7 @@ export function createCachedLoad( return async (args) => { const loadCacheKey = `${args.namespace}:${args.path}`; - let result: OnLoadResult | null | undefined = cache.get(loadCacheKey); + let result: OnLoadResult | null | undefined = await cache.get(loadCacheKey); if (result === undefined) { result = await callback(args); @@ -35,7 +35,9 @@ export function createCachedLoad( // Ensure requested path is included if it was a resolved file if (args.namespace === 'file') { result.watchFiles ??= []; - result.watchFiles.push(args.path); + if (!result.watchFiles.includes(args.path)) { + result.watchFiles.push(args.path); + } } await cache.put(loadCacheKey, result); } @@ -48,12 +50,26 @@ export function createCachedLoad( export class MemoryLoadResultCache implements LoadResultCache { #loadResults = new Map(); #fileDependencies = new Map>(); + #watchFilesPerKey = new Map>(); get(path: string): OnLoadResult | undefined { return this.#loadResults.get(path); } async put(path: string, result: OnLoadResult): Promise { + if (result.errors && result.errors.length > 0) { + const previousWatchFiles = this.#watchFilesPerKey.get(path); + if (previousWatchFiles) { + result.watchFiles = Array.from( + new Set([...(result.watchFiles ?? []), ...previousWatchFiles]), + ); + } + } else if (result.watchFiles && result.watchFiles.length > 0) { + this.#watchFilesPerKey.set(path, [...result.watchFiles]); + } else { + this.#watchFilesPerKey.delete(path); + } + this.#loadResults.set(path, result); if (result.watchFiles) { for (const watchFile of result.watchFiles) { @@ -90,4 +106,10 @@ export class MemoryLoadResultCache implements LoadResultCache { // are namespaced request paths and not disk-based file paths. return [...this.#fileDependencies.keys()]; } + + clear(): void { + this.#loadResults.clear(); + this.#fileDependencies.clear(); + this.#watchFilesPerKey.clear(); + } } diff --git a/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts b/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts new file mode 100644 index 000000000000..fb0396aea829 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/load-result-cache_spec.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { MemoryLoadResultCache } from './load-result-cache'; + +describe('MemoryLoadResultCache', () => { + let cache: MemoryLoadResultCache; + + beforeEach(() => { + cache = new MemoryLoadResultCache(); + }); + + it('should store and retrieve results', async () => { + const result = { + contents: 'body { color: red; }', + loader: 'css' as const, + }; + + await cache.put('file:/test/styles.css', result); + const cached = cache.get('file:/test/styles.css'); + + expect(cached).toBe(result); + }); + + it('should track watch files in fileDependencies', async () => { + const result = { + contents: 'body { color: red; }', + loader: 'css' as const, + watchFiles: ['/test/styles.css', '/test/theme.json'], + }; + + await cache.put('file:/test/styles.css', result); + + expect(cache.watchFiles).toContain('/test/styles.css'); + expect(cache.watchFiles).toContain('/test/theme.json'); + }); + + it('should invalidate cached results when a dependency changes', async () => { + const result = { + contents: 'body { color: red; }', + loader: 'css' as const, + watchFiles: ['/test/styles.css', '/test/theme.json'], + }; + + await cache.put('file:/test/styles.css', result); + expect(cache.get('file:/test/styles.css')).toBe(result); + + const invalidated = cache.invalidate('/test/theme.json'); + expect(invalidated).toBeTrue(); + expect(cache.get('file:/test/styles.css')).toBeUndefined(); + }); + + it('should preserve previous watch files when caching an error result', async () => { + const successResult = { + contents: 'body { color: red; }', + loader: 'css' as const, + watchFiles: ['/test/styles.css', '/test/theme.json'], + }; + + await cache.put('file:/test/styles.css', successResult); + cache.invalidate('/test/theme.json'); + + // Simulate an incremental rebuild error result that only has the entry file in watchFiles + const errorResult = { + errors: [{ text: 'Syntax error in theme.json' }], + watchFiles: ['/test/styles.css'], + }; + + await cache.put('file:/test/styles.css', errorResult); + + // Both the entry file and the previous dependency should be tracked + expect(cache.watchFiles).toContain('/test/styles.css'); + expect(cache.watchFiles).toContain('/test/theme.json'); + + // Invalidating the dependency should clear the cached error result + const invalidated = cache.invalidate('/test/theme.json'); + expect(invalidated).toBeTrue(); + expect(cache.get('file:/test/styles.css')).toBeUndefined(); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts new file mode 100644 index 000000000000..9d1e3ff5cfba --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts @@ -0,0 +1,381 @@ +/** + * @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 + * Implements a generic, two-tier (L1 memory + L2 persistent disk store) caching system for + * esbuild plugin `OnLoadResult` outcomes across the `@angular/build` compiler pipeline. + * + * Supported Module Types: + * 1. **Disk File Modules** (`file:` namespace): Standard disk-based source files (TS, JS, CSS, Sass, Less). + * Cache keys are computed using the root `globalConfigHash`, file path, and file content. Validity is + * verified via fast-path metadata (`mtimeMs` + `size`) with fallback to content hashing (`sha256`). + * 2. **Custom Plugin Namespace Modules** (e.g. `angular:script/global`, `sass:`): Modules loaded through + * custom esbuild namespaces that resolve to disk source files. + * 3. **Virtual Modules & Remote Resources** (e.g. `angular:styles/component`, `css-inline-fonts`): Synthetic + * in-memory modules or remote asset declarations whose compiled outcomes depend on parent source file + * dependencies (`watchFiles`) or global configuration options (`globalConfigHash`). + * + * Key Exported Types: + * - {@link PersistentLoadResultCache}: Primary two-tier cache manager implementing `LoadResultCache`. + * - {@link CachedLoadResultEntry}: Serialized structure persisted to disk for cached esbuild `OnLoadResult` items. + * - {@link CachedDependencyMetadata}: Per-dependency file metadata (`hash`, `mtimeMs`, `size`) used for cache validation and healing. + */ + +import type { Loader, OnLoadResult, PartialMessage } from 'esbuild'; +import { readFile, stat } from 'node:fs/promises'; +import { isAbsolute } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { calculateHash, createContentHash } from '../../utils/hash'; +import type { Cache as PersistentCacheStore } from './cache'; +import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache'; + +/** + * Metadata for a single watch file dependency. + */ +export interface CachedDependencyMetadata { + hash: string; + mtimeMs: number; + size: number; +} + +/** + * Serialized representation of any esbuild load result stored in persistent cache. + */ +export interface CachedLoadResultEntry { + /** Compiled output string or binary data */ + contents: string | Uint8Array; + + /** esbuild loader type */ + loader?: Loader; + + /** Absolute paths of all imported/watched dependency files */ + watchFiles: string[]; + + /** Absolute paths of all watched directories */ + watchDirs?: string[]; + + /** Map of watchFile absolute paths to dependency metadata */ + watchFilesMetadata: Record; + + /** Warnings emitted during load processing */ + warnings?: PartialMessage[]; + + /** Errors emitted during load processing */ + errors?: PartialMessage[]; +} + +/** + * Calculates a unique cache key by updating the hash incrementally. + * This prevents implicit string coercion of large binary content buffers. + */ +function calculateCacheKey( + globalConfigHash: string, + path: string, + content: string | Uint8Array, +): string { + const hasher = createContentHash(); + hasher.update(globalConfigHash); + hasher.update('\0'); + hasher.update(path); + hasher.update('\0'); + hasher.update(content); + + return hasher.digest(); +} + +/** + * Normalizes a namespaced cache key into a valid disk file path if one exists. + * Handles 'file:' URIs, OS platform differences, and custom plugin namespaces. + */ +export function extractDiskFilePath(path: string): string | undefined { + if (path.startsWith('file:')) { + const urlStr = path.startsWith('file://') ? path : 'file://' + path.slice(5); + try { + return fileURLToPath(urlStr); + } catch { + const candidate = path.slice(5); + + return isAbsolute(candidate) ? candidate : undefined; + } + } + + // Handle custom namespace prefix (e.g. "sass:/path/to/file") + // Ensure colonIndex > 1 to avoid treating Windows drive letters (e.g. "C:/") as namespace prefixes. + const colonIndex = path.indexOf(':'); + if (colonIndex > 1) { + const candidatePath = path.slice(colonIndex + 1); + if (isAbsolute(candidatePath)) { + return candidatePath; + } + } + + return isAbsolute(path) ? path : undefined; +} + +/** Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion. */ +const MAX_CONCURRENT_READS = 16; + +/** + * Maps an array asynchronously with a sliding worker pool to maintain full concurrency saturation. + */ +async function mapConcurrent( + items: T[], + limit: number, + fn: (item: T) => Promise, +): Promise { + const results: R[] = new Array(items.length); + let index = 0; + + const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { + while (index < items.length) { + const i = index++; + results[i] = await fn(items[i]); + } + }); + + await Promise.all(workers); + + return results; +} + +/** + * Validates that all imported watch files exist on disk and their contents match. + * Performs a fast-path metadata check (mtime + size) first, falling back to content hashing. + * Heals/updates the cached metadata on disk if the content hash was valid but the metadata changed. + */ +async function validateAndHealCacheEntry( + watchFilesMetadata: Record | undefined, + store: PersistentCacheStore, + cacheKey: string, + cached: CachedLoadResultEntry, + targetFilePath?: string, +): Promise { + if (!watchFilesMetadata) { + return false; + } + + const watchFiles = Object.keys(watchFilesMetadata); + let healed = false; + + const isValidResults = await mapConcurrent(watchFiles, MAX_CONCURRENT_READS, async (filePath) => { + try { + const stats = await stat(filePath); + const expected = watchFilesMetadata[filePath]; + if (!expected) { + return false; + } + + // 1. Fast Path: size and mtime match + if (stats.size === expected.size && stats.mtimeMs === expected.mtimeMs) { + return true; + } + + // 2. Target File Path: content hash was already verified by cacheKey lookup, heal metadata if mtime changed + if (targetFilePath && filePath === targetFilePath) { + watchFilesMetadata[filePath] = { + ...expected, + mtimeMs: stats.mtimeMs, + size: stats.size, + }; + healed = true; + + return true; + } + + // 3. Slow Path for dependencies: content hash fallback + const currentContent = await readFile(filePath); + const currentHash = calculateHash(currentContent); + if (currentHash === expected.hash) { + // Heal cache entry with new metadata + watchFilesMetadata[filePath] = { + ...expected, + mtimeMs: stats.mtimeMs, + size: stats.size, + }; + healed = true; + + return true; + } + + return false; + } catch { + return false; + } + }); + + if (isValidResults.some((isValid) => !isValid)) { + return false; + } + + if (healed) { + try { + await store.put(cacheKey, cached); + } catch { + // Ignore errors writing healed entries + } + } + + return true; +} + +/** + * Computes metadata (content hashes, mtime, size) for an array of watch file paths. + * Processes files with a sliding worker pool of 16 concurrent operations. + */ +async function computeMetadataForWatchFiles( + watchFiles: string[], + knownContents?: Map, +): Promise> { + const watchFilesMetadata: Record = {}; + + await mapConcurrent(watchFiles, MAX_CONCURRENT_READS, async (filePath) => { + try { + const knownContent = knownContents?.get(filePath); + const [content, stats] = await Promise.all([ + knownContent !== undefined ? knownContent : readFile(filePath), + stat(filePath), + ]); + const hash = calculateHash(content); + watchFilesMetadata[filePath] = { + hash, + mtimeMs: stats.mtimeMs, + size: stats.size, + }; + } catch { + // Ignore unreadable files + } + }); + + return watchFilesMetadata; +} + +export class PersistentLoadResultCache implements LoadResultCache { + private readonly memoryCache = new MemoryLoadResultCache(); + + constructor( + private readonly persistentStore?: PersistentCacheStore, + private readonly globalConfigHash: string = '', + ) {} + + /** + * Retrieves a load result from cache. + * Checks L1 memory cache first for immediate watch-mode speed, falling back to L2 persistent disk + * store on L1 cache miss. L2 persistent cache entries are validated against dependency metadata. + */ + async get(path: string): Promise { + // 1. Check L1 Memory Cache + const memoryResult = this.memoryCache.get(path); + if (memoryResult) { + return memoryResult; + } + + if (!this.persistentStore) { + return undefined; + } + + // 2. Check L2 Persistent Disk Cache + let content: string | Uint8Array = ''; + const filePath = extractDiskFilePath(path); + if (filePath) { + try { + content = await readFile(filePath); + } catch { + return undefined; + } + } + + const cacheKey = calculateCacheKey(this.globalConfigHash, path, content); + const cached = await this.persistentStore.get(cacheKey); + + if ( + cached && + (await validateAndHealCacheEntry( + cached.watchFilesMetadata, + this.persistentStore, + cacheKey, + cached, + filePath, + )) + ) { + const result: OnLoadResult = { + contents: cached.contents, + loader: cached.loader, + watchFiles: cached.watchFiles, + watchDirs: cached.watchDirs, + warnings: cached.warnings, + errors: cached.errors, + }; + + // Populate L1 Memory Cache for subsequent lookups + await this.memoryCache.put(path, result); + + return result; + } + + return undefined; + } + + /** + * Stores a load result in both L1 memory cache and L2 persistent disk store. + */ + async put(path: string, result: OnLoadResult): Promise { + await this.memoryCache.put(path, result); + + // Persist to L2 store if persistentStore is configured and contents exist (including empty strings/buffers) + if (this.persistentStore && result.contents !== undefined) { + let content: string | Uint8Array = ''; + const filePath = extractDiskFilePath(path); + if (filePath) { + try { + content = await readFile(filePath); + } catch { + // Skip L2 persistent store if target disk file cannot be read + return; + } + } + + const cacheKey = calculateCacheKey(this.globalConfigHash, path, content); + + // Reuse the target file's pre-read content buffer to avoid redundant disk reads (readFile) + // during dependency watch file metadata computation. + const knownContents = filePath + ? new Map([[filePath, content]]) + : undefined; + const watchFilesMetadata = await computeMetadataForWatchFiles( + result.watchFiles ?? [], + knownContents, + ); + + await this.persistentStore.put(cacheKey, { + contents: result.contents, + loader: result.loader, + watchFiles: result.watchFiles ?? [], + watchDirs: result.watchDirs, + watchFilesMetadata, + warnings: result.warnings, + errors: result.errors, + }); + } + } + + /** + * Invalidates cached entries affected by a modified dependency file during watch mode. + * + * Note: Invalidation of L1 memory cache is sufficient for active watch mode. + * Cross-process/cold start stale entries in L2 persistent store are automatically handled + * during `get()` via dependency metadata verification (`validateAndHealCacheEntry`). + */ + invalidate(path: string): boolean { + return this.memoryCache.invalidate(path); + } + + get watchFiles(): ReadonlyArray { + return this.memoryCache.watchFiles; + } +} diff --git a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts new file mode 100644 index 000000000000..f8ecc0611818 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts @@ -0,0 +1,263 @@ +/** + * @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 { OnLoadResult } from 'esbuild'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { initializeHash } from '../../utils/hash'; +import type { Cache as PersistentCacheStore } from './cache'; +import { + type CachedLoadResultEntry, + PersistentLoadResultCache, + extractDiskFilePath, +} from './persistent-load-result-cache'; + +describe('extractDiskFilePath', () => { + it('should extract disk file path for file: URIs', () => { + const filePath = '/Users/test/project/file.js'; + expect(extractDiskFilePath(`file://${filePath}`)).toBe(filePath); + }); + + it('should extract disk file path for custom plugin namespaces', () => { + const filePath = '/Users/test/project/file.js'; + expect(extractDiskFilePath(`sass:${filePath}`)).toBe(filePath); + }); + + it('should not strip Windows drive letter as a namespace prefix', () => { + const winPath = 'C:/Users/test/project/file.js'; + expect(extractDiskFilePath(winPath)).not.toBe('/Users/test/project/file.js'); + }); +}); + +describe('PersistentLoadResultCache', () => { + let mockStore: Map; + let persistentStore: PersistentCacheStore; + let tmpDir: string; + let file1: string; + + beforeAll(async () => { + await initializeHash(); + }); + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'persistent-cache-test-')); + file1 = path.join(tmpDir, 'test.js'); + fs.writeFileSync(file1, 'console.log("hello");'); + + mockStore = new Map(); + persistentStore = { + async get(key: string) { + return mockStore.get(key); + }, + async put(key: string, value: CachedLoadResultEntry) { + mockStore.set(key, value); + }, + } as unknown as PersistentCacheStore; + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('should return undefined on L1 and L2 cache miss', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const result = await cache.get(file1); + expect(result).toBeUndefined(); + }); + + it('should hit L2 persistent store and return cached output when dependencies are valid', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const sampleResult: OnLoadResult = { + contents: 'console.log("hello");', + loader: 'js', + watchFiles: [file1], + }; + + await cache.put(file1, sampleResult); + + // Create a second cache instance (simulating cold start) + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(file1); + + expect(hit).toBeDefined(); + expect(hit?.contents).toBe('console.log("hello");'); + expect(hit?.loader).toBe('js'); + }); + + it('should hit L2 persistent store and return cached output for empty file contents', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const sampleResult: OnLoadResult = { + contents: '', + loader: 'css', + watchFiles: [file1], + }; + + await cache.put(file1, sampleResult); + + // Create a second cache instance (simulating cold start) + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(file1); + + expect(hit).toBeDefined(); + expect(hit?.contents).toBe(''); + expect(hit?.loader).toBe('css'); + }); + + it('should preserve watchDirs when hitting L2 persistent store', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const sampleResult: OnLoadResult = { + contents: 'console.log("hello");', + loader: 'js', + watchFiles: [file1], + watchDirs: [tmpDir], + }; + + await cache.put(file1, sampleResult); + + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(file1); + + expect(hit).toBeDefined(); + expect(hit?.watchDirs).toEqual([tmpDir]); + }); + + it('should hit L2 persistent store for custom plugin namespaces backed by disk files', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const customPath = `sass:${file1}`; + const sampleResult: OnLoadResult = { + contents: '.btn { color: red; }', + loader: 'css', + watchFiles: [file1], + }; + + await cache.put(customPath, sampleResult); + + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(customPath); + + expect(hit).toBeDefined(); + expect(hit?.contents).toBe('.btn { color: red; }'); + expect(hit?.loader).toBe('css'); + }); + + it('should hit L2 persistent store for virtual modules without disk representation', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const virtualPath = 'angular:styles/component:css;0;data'; + const sampleResult: OnLoadResult = { + contents: 'h1 { margin: 0; }', + loader: 'css', + watchFiles: [file1], + }; + + await cache.put(virtualPath, sampleResult); + + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(virtualPath); + + expect(hit).toBeDefined(); + expect(hit?.contents).toBe('h1 { margin: 0; }'); + expect(hit?.loader).toBe('css'); + }); + + it('should invalidate L2 persistent cache hit if a watch dependency file is modified', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const sampleResult: OnLoadResult = { + contents: 'console.log("hello");', + loader: 'js', + watchFiles: [file1], + }; + + await cache.put(file1, sampleResult); + + // Modify dependency file content + fs.writeFileSync(file1, 'console.log("world");'); + + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(file1); + + expect(hit).toBeUndefined(); + }); + + it('should heal the cache entry with new metadata if content hash is still valid after mtime changes', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const sampleResult: OnLoadResult = { + contents: 'console.log("hello");', + loader: 'js', + watchFiles: [file1], + }; + + await cache.put(file1, sampleResult); + + // Retrieve the cache key from store + const keys = Array.from(mockStore.keys()); + expect(keys.length).toBe(1); + const cacheKey = keys[0]; + + const initialEntry = mockStore.get(cacheKey); + const initialMtime = initialEntry?.watchFilesMetadata[file1].mtimeMs; + expect(initialMtime).toBeDefined(); + + // Artificially change file modification time without changing content + const futureTime = new Date(Date.now() + 50000); + fs.utimesSync(file1, futureTime, futureTime); + + const statsAfter = fs.statSync(file1); + expect(statsAfter.mtimeMs).not.toEqual(initialMtime as number); + + // Query cold cache to trigger fallback content hash check and healing + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(file1); + + expect(hit).toBeDefined(); + expect(hit?.contents).toBe('console.log("hello");'); + + // Verify metadata was healed in-place in the persistent store + const healedEntry = mockStore.get(cacheKey); + expect(healedEntry?.watchFilesMetadata[file1].mtimeMs).toEqual(statsAfter.mtimeMs); + }); + + it('should safely handle corrupted cache entries with missing watchFilesMetadata', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const sampleResult: OnLoadResult = { + contents: 'console.log("hello");', + loader: 'js', + watchFiles: [file1], + }; + + await cache.put(file1, sampleResult); + + // Corrupt entry in store by removing watchFilesMetadata + const keys = Array.from(mockStore.keys()); + expect(keys.length).toBe(1); + const entry = mockStore.get(keys[0]); + expect(entry).toBeDefined(); + if (entry) { + delete (entry as Partial).watchFilesMetadata; + } + + const coldCache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const hit = await coldCache.get(file1); + + expect(hit).toBeUndefined(); + }); + + it('should skip L2 store put if target file cannot be read from disk', async () => { + const cache = new PersistentLoadResultCache(persistentStore, 'global-hash'); + const nonExistentFile = path.join(tmpDir, 'non-existent.js'); + const sampleResult: OnLoadResult = { + contents: 'console.log("missing");', + loader: 'js', + watchFiles: [], + }; + + await cache.put(nonExistentFile, sampleResult); + + expect(mockStore.size).toBe(0); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/profiling.ts b/packages/angular/build/src/tools/esbuild/profiling.ts index 2e67a4cb27f2..80fd26c32d0e 100644 --- a/packages/angular/build/src/tools/esbuild/profiling.ts +++ b/packages/angular/build/src/tools/esbuild/profiling.ts @@ -14,6 +14,32 @@ export function resetCumulativeDurations(): void { cumulativeDurations?.clear(); } +export function getAndClearCumulativeDurations(): Record | undefined { + if (!cumulativeDurations || cumulativeDurations.size === 0) { + return undefined; + } + + const data = Object.fromEntries(cumulativeDurations); + + cumulativeDurations.clear(); + + return data; +} + +export function mergeCumulativeDurations(data: Record): void { + cumulativeDurations ??= new Map(); + + for (const [name, durations] of Object.entries(data)) { + let existing = cumulativeDurations.get(name); + if (!existing) { + existing = []; + cumulativeDurations.set(name, existing); + } + + existing.push(...durations); + } +} + export function logCumulativeDurations(): void { if (!debugPerformance || !cumulativeDurations) { return; diff --git a/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts b/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts new file mode 100644 index 000000000000..4f50515749df --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts @@ -0,0 +1,169 @@ +/** + * @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 { DatabaseSync, StatementSync } from 'node:sqlite'; +import { Cache, PersistentCacheStore } from './cache'; + +export class SqliteCacheStore implements PersistentCacheStore { + #db: DatabaseSync | undefined; + #getStmt: StatementSync | undefined; + #hasStmt: StatementSync | undefined; + #setStmt: StatementSync | undefined; + #updateAccessedStmt: StatementSync | undefined; + readonly #pendingAccessedKeys = new Set(); + #flushTimeout: NodeJS.Timeout | undefined; + + constructor( + readonly cachePath: string, + private readonly maxPayloadSize = 1024 * 1024 * 1024, + private readonly ttlDays = 14, + ) {} + + #ensureDb(): DatabaseSync { + if (!this.#db) { + this.#db = new DatabaseSync(this.cachePath); + // Optimize SQLite for cache usage + this.#db.exec('PRAGMA auto_vacuum = FULL;'); + this.#db.exec('PRAGMA journal_mode = WAL;'); + this.#db.exec('PRAGMA synchronous = NORMAL;'); + this.#db.exec('PRAGMA busy_timeout = 5000;'); + this.#db.exec('PRAGMA temp_store = MEMORY;'); + this.#db.exec('PRAGMA mmap_size = 268435456;'); + this.#db.exec( + 'CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, last_accessed INTEGER NOT NULL) WITHOUT ROWID;', + ); + + this.#getStmt = this.#db.prepare('SELECT value FROM cache WHERE key = ?'); + this.#hasStmt = this.#db.prepare('SELECT 1 FROM cache WHERE key = ?'); + this.#setStmt = this.#db.prepare( + 'INSERT OR REPLACE INTO cache (key, value, last_accessed) VALUES (?, ?, unixepoch())', + ); + this.#updateAccessedStmt = this.#db.prepare( + 'UPDATE cache SET last_accessed = unixepoch() WHERE key = ?', + ); + } + + return this.#db; + } + + #queueAccessUpdate(key: string): void { + this.#pendingAccessedKeys.add(key); + + if (this.#pendingAccessedKeys.size >= 100) { + this.#flushAccessUpdates(); + } else if (!this.#flushTimeout) { + this.#flushTimeout = setTimeout(() => this.#flushAccessUpdates(), 500); + this.#flushTimeout.unref?.(); + } + } + + #flushAccessUpdates(): void { + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout); + this.#flushTimeout = undefined; + } + + if (!this.#db || this.#pendingAccessedKeys.size === 0 || !this.#updateAccessedStmt) { + return; + } + + try { + this.#db.exec('BEGIN IMMEDIATE TRANSACTION;'); + for (const key of this.#pendingAccessedKeys) { + this.#updateAccessedStmt.run(key); + } + this.#db.exec('COMMIT;'); + } catch { + try { + this.#db.exec('ROLLBACK;'); + } catch { + // Ignore rollback errors if transaction was not active + } + } finally { + this.#pendingAccessedKeys.clear(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async get(key: string): Promise { + this.#ensureDb(); + const row = this.#getStmt?.get(key) as { value: string } | undefined; + + if (row) { + this.#queueAccessUpdate(key); + + try { + return JSON.parse(row.value); + } catch { + return undefined; + } + } + + return undefined; + } + + has(key: string): boolean { + this.#ensureDb(); + + return !!this.#hasStmt?.get(key); + } + + async set(key: string, value: unknown): Promise { + this.#ensureDb(); + this.#pendingAccessedKeys.delete(key); + this.#setStmt?.run(key, JSON.stringify(value)); + + return this; + } + + createCache(namespace: string): Cache { + return new Cache(this, namespace); + } + + close(): void { + if (this.#db) { + try { + // Flush any pending access updates in one transaction before pruning + this.#flushAccessUpdates(); + + // 1. Delete items older than N days + this.#db + .prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);") + .run(`-${this.ttlDays} days`); + + // 2. Prune oldest items if payload exceeds maxPayloadSize + const pruneStmt = this.#db.prepare(` + DELETE FROM cache WHERE key IN ( + SELECT key FROM ( + SELECT key, + sum(length(key) + length(value)) OVER (ORDER BY last_accessed DESC, key DESC) as running_size + FROM cache + ) WHERE running_size > ? + ); + `); + pruneStmt.run(this.maxPayloadSize); + } catch { + // Pruning errors should not block build success + } finally { + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout); + this.#flushTimeout = undefined; + } + this.#pendingAccessedKeys.clear(); + + this.#getStmt = undefined; + this.#hasStmt = undefined; + this.#setStmt = undefined; + this.#updateAccessedStmt = undefined; + + this.#db.close(); + this.#db = undefined; + } + } + } +} diff --git a/packages/angular/build/src/tools/esbuild/sqlite-cache-store_spec.ts b/packages/angular/build/src/tools/esbuild/sqlite-cache-store_spec.ts new file mode 100644 index 000000000000..679bff21de20 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/sqlite-cache-store_spec.ts @@ -0,0 +1,178 @@ +/** + * @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 { promises as fs } from 'node:fs'; +import { join } from 'node:path'; +import { SqliteCacheStore } from './sqlite-cache-store'; + +describe('SqliteCacheStore', () => { + let tempDir: string; + let cachePath: string; + let store: SqliteCacheStore; + + beforeEach(async () => { + // Create a temporary directory in the workspace for testing + tempDir = join(__dirname, `sqlite-test-temp-${Date.now()}`); + await fs.mkdir(tempDir, { recursive: true }); + cachePath = join(tempDir, 'test-cache.db'); + store = new SqliteCacheStore(cachePath); + }); + + afterEach(async () => { + store.close(); + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('should store and retrieve a value', async () => { + const data = { foo: 'bar', list: [1, 2, 3] }; + await store.set('test-key', data); + + const result = await store.get('test-key'); + expect(result).toEqual(data); + }); + + it('should return undefined for non-existent key', async () => { + const result = await store.get('missing-key'); + expect(result).toBeUndefined(); + }); + + it('should correctly report existence of a key', async () => { + expect(store.has('exist-key')).toBeFalse(); + + await store.set('exist-key', 'value'); + expect(store.has('exist-key')).toBeTrue(); + }); + + it('should overwrite values for existing keys', async () => { + await store.set('overwrite-key', 'initial'); + await store.set('overwrite-key', 'updated'); + + const result = await store.get('overwrite-key'); + expect(result).toBe('updated'); + }); + + it('should prune items older than TTL on close', async () => { + // Write two items + await store.set('new-key', 'new-val'); + await store.set('old-key', 'old-val'); + + // Close the store so we can modify the DB safely + store.close(); + + // Directly open database to update timestamp of 'old-key' to 15 days ago + const { DatabaseSync } = await import('node:sqlite'); + const directDb = new DatabaseSync(cachePath); + directDb + .prepare('UPDATE cache SET last_accessed = unixepoch() - 15 * 24 * 3600 WHERE key = ?') + .run('old-key'); + directDb.close(); + + // Reopen store with a 14-day TTL, access it to open connection, then close to trigger pruning + const pruneStore = new SqliteCacheStore(cachePath, undefined, 14); + expect(pruneStore.has('new-key')).toBeTrue(); + pruneStore.close(); + + // Verify 'old-key' is gone but 'new-key' remains + const checkStore = new SqliteCacheStore(cachePath); + expect(checkStore.has('old-key')).toBeFalse(); + expect(checkStore.has('new-key')).toBeTrue(); + checkStore.close(); + }); + + it('should prune oldest items when total payload size exceeds maximum on close', async () => { + // Close the default store so we can instantiate one with a small limit + store.close(); + + // Create a store with a tiny size limit (e.g. 25 bytes) + // Keys 'k1', 'k2', 'k3' are small (each is 10 bytes: key + JSON.stringify(value)). + // Total size of k1 + k2 + k3 is 30 bytes, which exceeds the 25 bytes limit. + const sizeStore = new SqliteCacheStore(cachePath, 25); + + // Set k1, then k2, then k3. + // Order of inserts: k1 (oldest), k2 (middle), k3 (newest) + await sizeStore.set('k1', 'value1'); + await sizeStore.set('k2', 'value2'); + await sizeStore.set('k3', 'value3'); + + // Close sizeStore to trigger pruning + sizeStore.close(); + + // Reopen to check which keys were kept + const checkStore = new SqliteCacheStore(cachePath); + // k3 (newest) and k2 (middle) should be kept (~20 bytes total) + // k1 (oldest) should be pruned to get under 25 bytes. + expect(checkStore.has('k3')).toBeTrue(); + expect(checkStore.has('k2')).toBeTrue(); + expect(checkStore.has('k1')).toBeFalse(); + checkStore.close(); + }); + + describe('NG_BUILD_CACHE_STORE env variable option', () => { + it('should force SQLite when NG_BUILD_CACHE_STORE=sqlite', () => { + const code = ` + (async () => { + const { createPersistentCacheStore } = await import('./cache.js'); + const { SqliteCacheStore } = await import('./sqlite-cache-store.js'); + const store = await createPersistentCacheStore('dummy-sqlite-env'); + if (!(store instanceof SqliteCacheStore)) { + console.error('Expected SqliteCacheStore, got:', store.constructor.name); + process.exit(1); + } + })().catch(err => { + console.error(err); + process.exit(2); + }); + `; + const { execFileSync } = require('node:child_process'); + execFileSync(process.execPath, ['--input-type=module', '-e', code], { + cwd: __dirname, + env: { + ...process.env, + NG_BUILD_CACHE_STORE: 'sqlite', + }, + }); + }); + + it('should force LMDB when NG_BUILD_CACHE_STORE=lmdb', () => { + const code = ` + (async () => { + const { createPersistentCacheStore } = await import('./cache.js'); + const { LmdbCacheStore } = await import('./lmdb-cache-store.js'); + const store = await createPersistentCacheStore('dummy-lmdb-env'); + if (!(store instanceof LmdbCacheStore)) { + console.error('Expected LmdbCacheStore, got:', store.constructor.name); + process.exit(1); + } + })().catch(err => { + console.error(err); + process.exit(2); + }); + `; + const { execFileSync } = require('node:child_process'); + try { + execFileSync(process.execPath, ['--input-type=module', '-e', code], { + cwd: __dirname, + env: { + ...process.env, + NG_BUILD_CACHE_STORE: 'lmdb', + }, + }); + } catch (e) { + if (e && typeof e === 'object' && 'message' in e) { + const error = e as { message: string; stderr?: Buffer }; + const output = error.stderr?.toString() || error.message; + if (!output.includes('Unable to initialize JavaScript cache storage')) { + throw e; + } + } else { + throw e; + } + } + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-cache-key.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-cache-key.ts new file mode 100644 index 000000000000..22397ab189b3 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-cache-key.ts @@ -0,0 +1,68 @@ +/** + * @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 { calculateHash } from '../../../utils/hash'; +import type { BundleStylesheetOptions } from './bundle-options'; + +/** + * Generates a global hash based on all build options that affect stylesheet compilation. + * + * IMPORTANT: This hash acts as the root cache key prefix for all persistent stylesheet cache + * entries. Any change in the values hashed below will invalidate all cached stylesheet outputs + * across the entire application. + * + * Included build options and their rationale: + * - `optimization`: Affects CSS minification, dead code elimination, and whitespace stripping. + * - `sourcemap`: Toggles inline/external source map generation and comment insertion. + * - `sourcesContent`: Toggles embedding original source content inside generated source maps. + * - `includePaths`: Changes Sass/Less `@import` and `@use` path resolution search directories. + * - `sassOptions`: Changes Sass compiler deprecations and behavior flags. + * - `target`: Affects CSS property lowering and browser vendor prefixing in esbuild. + * - `publicPath`: Affects relative asset URL rewriting (`url('...')`) inside CSS output. + * - `outputNames`: Affects asset output filename hashing schemes. + * - `inlineFonts`: Controls whether external web font `@import` / `` directives are inlined. + * - `preserveSymlinks`: Controls symlink realpath resolution in monorepos/pnpm workspace packages. + * - `externalDependencies`: Controls which CSS modules/urls are excluded from bundling. + * - `postcssConfig`: Path to custom PostCSS configuration file. + * - `tailwindConfig`: Path to Tailwind CSS configuration file. + * - `packageVersion`: Invalidates cache across `@angular/build` compiler toolchain updates. + * + * @note Maintainers: If any new build option is added to `BundleStylesheetOptions` or `@angular/build` + * that alters generated stylesheet code or asset outputs, it MUST be added to this hash object. + */ +export function calculateGlobalStylesheetConfigHash( + options: BundleStylesheetOptions, + packageVersion: string = '', +): string { + return calculateHash( + JSON.stringify({ + optimization: options.optimization, + sourcemap: options.sourcemap, + sourcesContent: options.sourcesContent, + includePaths: options.includePaths, + sassOptions: options.sass + ? { + futureDeprecations: options.sass.futureDeprecations, + fatalDeprecations: options.sass.fatalDeprecations, + silenceDeprecations: options.sass.silenceDeprecations, + } + : undefined, + target: options.target, + publicPath: options.publicPath, + outputNames: options.outputNames, + inlineFonts: options.inlineFonts, + preserveSymlinks: options.preserveSymlinks, + externalDependencies: options.externalDependencies, + postcssConfig: options.postcssConfiguration?.configPath + ? options.postcssConfiguration.configPath + : '', + tailwindConfig: options.tailwindConfiguration?.file ? options.tailwindConfiguration.file : '', + packageVersion, + }), + ); +} diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-cache-key_spec.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-cache-key_spec.ts new file mode 100644 index 000000000000..c524a2d0c36b --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-cache-key_spec.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 { initializeHash } from '../../../utils/hash'; +import type { BundleStylesheetOptions } from './bundle-options'; +import { calculateGlobalStylesheetConfigHash } from './stylesheet-cache-key'; + +describe('Stylesheet Global Config Hash', () => { + beforeAll(async () => { + await initializeHash(); + }); + + const baseOptions: BundleStylesheetOptions = { + workspaceRoot: '/root', + optimization: true, + inlineFonts: false, + sourcemap: true, + outputNames: { bundles: 'styles', media: 'media' }, + target: ['chrome100'], + cacheOptions: { enabled: true, path: '/cache', basePath: '/root' }, + }; + + describe('calculateGlobalStylesheetConfigHash', () => { + it('should generate consistent hashes for identical options', () => { + const hash1 = calculateGlobalStylesheetConfigHash(baseOptions, '1.0.0'); + const hash2 = calculateGlobalStylesheetConfigHash({ ...baseOptions }, '1.0.0'); + expect(hash1).toBe(hash2); + }); + + it('should produce different hashes when optimization changes', () => { + const hash1 = calculateGlobalStylesheetConfigHash(baseOptions, '1.0.0'); + const hash2 = calculateGlobalStylesheetConfigHash( + { ...baseOptions, optimization: false }, + '1.0.0', + ); + expect(hash1).not.toBe(hash2); + }); + + it('should produce different hashes when sourcemap options change', () => { + const hash1 = calculateGlobalStylesheetConfigHash(baseOptions, '1.0.0'); + const hash2 = calculateGlobalStylesheetConfigHash( + { ...baseOptions, sourcemap: false }, + '1.0.0', + ); + expect(hash1).not.toBe(hash2); + }); + + it('should produce different hashes when sourcesContent changes', () => { + const hash1 = calculateGlobalStylesheetConfigHash( + { ...baseOptions, sourcesContent: true }, + '1.0.0', + ); + const hash2 = calculateGlobalStylesheetConfigHash( + { ...baseOptions, sourcesContent: false }, + '1.0.0', + ); + expect(hash1).not.toBe(hash2); + }); + + it('should produce different hashes when target browsers change', () => { + const hash1 = calculateGlobalStylesheetConfigHash(baseOptions, '1.0.0'); + const hash2 = calculateGlobalStylesheetConfigHash( + { ...baseOptions, target: ['firefox90'] }, + '1.0.0', + ); + expect(hash1).not.toBe(hash2); + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts index 78925f35835e..2c40e007350e 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts @@ -427,6 +427,7 @@ async function compileString( }, }, ], + watchFiles: error.file && error.file !== filename ? [filename, error.file] : [filename], }; } else { assertIsError(error); diff --git a/packages/angular/build/src/tools/esbuild/target.ts b/packages/angular/build/src/tools/esbuild/target.ts new file mode 100644 index 000000000000..2925905276b7 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/target.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { coerce, compare, minVersion } from 'semver'; + +/** + * Compares two target version strings. + * + * This function is used to determine the lowest version for a given browser target. + * + * @param a The first version string. + * @param b The second version string. + * @returns A negative value if `a` is lower than `b`, a positive value if `a` is higher than `b`, and 0 if they are equal. + */ +function compareTargetVersions(a: string, b: string): number { + const aVersion = coerce(a); + const bVersion = coerce(b); + + if (!aVersion || !bVersion) { + return aVersion ? -1 : bVersion ? 1 : 0; + } + + return compare(aVersion, bVersion); +} + +// https://esbuild.github.io/api/#target +const ESBUILD_SUPPORTED_BROWSERS: ReadonlySet = new Set([ + 'chrome', + 'edge', + 'firefox', + 'ie', + 'ios', + 'node', + 'opera', + 'safari', +]); + +/** + * Transform browserlists result to esbuild target. + * + * Only the lowest version for each browser is returned to avoid issues with esbuild and rolldown + * when multiple versions of the same target engine are specified. + * + * @see https://esbuild.github.io/api/#target + * @see https://github.com/evanw/esbuild/issues/4509 + * @see https://github.com/rolldown/rolldown/issues/10633 + */ +export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] { + const browsers = new Map(); + + for (const browser of supportedBrowsers) { + let [browserName, version] = browser.toLowerCase().split(' '); + if (!browserName || !version) { + continue; + } + + // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` + if (browserName === 'ios_saf') { + browserName = 'ios'; + } + + if (!ESBUILD_SUPPORTED_BROWSERS.has(browserName)) { + continue; + } + + // browserslist uses ranges `15.2-15.3` versions but only the lowest is required + // to perform minimum supported feature checks. esbuild also expects a single version. + [version] = version.split('-'); + + if (browserName === 'safari' && version === 'tp') { + // esbuild only supports numeric versions so `TP` is converted to a high number (999) since + // a Technology Preview (TP) of Safari is assumed to support all currently known features. + version = '999'; + } else if (!version.includes('.')) { + // A lone major version is considered by esbuild to include all minor versions. However, + // browserslist does not and is also inconsistent in its `.0` version naming. For example, + // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`. + version += '.0'; + } + + const current = browsers.get(browserName); + if (!current || compareTargetVersions(version, current) < 0) { + browsers.set(browserName, version); + } + } + + return Array.from(browsers, ([browserName, version]) => browserName + version); +} + +const SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE'; + +/** + * Transform supported Node.js versions to esbuild target. + * + * Only the lowest Node.js version is returned to avoid issues with esbuild and rolldown + * when multiple versions of the same target engine are specified. + * + * @see https://esbuild.github.io/api/#target + * @see https://github.com/evanw/esbuild/issues/4509 + * @see https://github.com/rolldown/rolldown/issues/10633 + */ +export function getSupportedNodeTargets(): string[] { + if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') { + // Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions. + return []; + } + + const parsed = minVersion(SUPPORTED_NODE_VERSIONS); + + return parsed ? ['node' + parsed.version] : []; +} diff --git a/packages/angular/build/src/tools/esbuild/target_spec.ts b/packages/angular/build/src/tools/esbuild/target_spec.ts new file mode 100644 index 000000000000..e5a375a1412b --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/target_spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { getSupportedNodeTargets, transformSupportedBrowsersToTargets } from './target'; + +describe('esbuild target', () => { + describe('transformSupportedBrowsersToTargets', () => { + it('should return the smallest version for each browser', () => { + const targets = transformSupportedBrowsersToTargets([ + 'chrome 122', + 'chrome 120', + 'chrome 121', + 'firefox 116', + 'firefox 115', + 'safari 17.0', + 'safari 16.4', + ]); + + expect(targets).toEqual(['chrome120.0', 'firefox115.0', 'safari16.4']); + }); + + it('should handle version ranges and pick the lowest version', () => { + const targets = transformSupportedBrowsersToTargets([ + 'ios_saf 15.4', + 'ios_saf 15.2-15.3', + 'ios_saf 16.0', + ]); + + expect(targets).toEqual(['ios15.2']); + }); + + it('should handle Safari TP (Technology Preview)', () => { + const targetsWithOlderSafari = transformSupportedBrowsersToTargets([ + 'safari TP', + 'safari 16.4', + ]); + expect(targetsWithOlderSafari).toEqual(['safari16.4']); + + const targetsWithOnlyTP = transformSupportedBrowsersToTargets(['safari TP']); + expect(targetsWithOnlyTP).toEqual(['safari999']); + }); + + it('should ignore browsers not supported by esbuild', () => { + const targets = transformSupportedBrowsersToTargets([ + 'android 4.4', + 'samsung 22', + 'kaios 2.5', + 'chrome 115', + ]); + + expect(targets).toEqual(['chrome115.0']); + }); + + it('should return empty array for empty supportedBrowsers', () => { + const targets = transformSupportedBrowsersToTargets([]); + expect(targets).toEqual([]); + }); + + it('should handle malformed or incomplete browser strings gracefully', () => { + const targets = transformSupportedBrowsersToTargets(['chrome', 'firefox ', '']); + expect(targets).toEqual([]); + }); + + it('should handle single major versions by appending .0', () => { + const targets = transformSupportedBrowsersToTargets(['chrome 120', 'edge 120']); + expect(targets).toEqual(['chrome120.0', 'edge120.0']); + }); + }); + + describe('getSupportedNodeTargets', () => { + it('should return empty array when node versions are not stamped', () => { + expect(getSupportedNodeTargets()).toEqual([]); + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts index 9024a981c3f7..7d9dce4a5522 100644 --- a/packages/angular/build/src/tools/esbuild/utils.ts +++ b/packages/angular/build/src/tools/esbuild/utils.ts @@ -9,11 +9,9 @@ import { BuilderContext } from '@angular-devkit/architect'; import { BuildOptions, Metafile, OutputFile, formatMessages } from 'esbuild'; import { Listr } from 'listr2'; -import { createHash } from 'node:crypto'; import { basename, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { brotliCompress } from 'node:zlib'; -import { coerce } from 'semver'; import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { OutputMode } from '../../builders/application/schema'; import { BudgetCalculatorResult } from '../../utils/bundle-calculator'; @@ -27,7 +25,7 @@ import { import { type BuildOutputFile, BuildOutputFileType, type InitialFileRecord } from './bundler-files'; export function logBuildStats( - metafile: Metafile, + metafiles: Metafile[], outputFiles: BuildOutputFile[], initial: Map, budgetFailures: BudgetCalculatorResult[] | undefined, @@ -67,12 +65,12 @@ export function logBuildStats( } // Skip logging external component stylesheets used for HMR - if (metafile.outputs[file] && 'ng-component' in metafile.outputs[file]) { + if (metafiles.some((mf) => mf.outputs[file] && 'ng-component' in mf.outputs[file])) { componentStyleChange = true; continue; } - const name = initial.get(file)?.name ?? getChunkNameFromMetafile(metafile, file); + const name = initial.get(file)?.name ?? getChunkNameFromMetafile(metafiles, file); const stat: BundleStats = { initial: initial.has(file), stats: [file, name ?? '-', size, estimatedTransferSizes?.get(file) ?? '-'], @@ -110,9 +108,15 @@ export function logBuildStats( return ''; } -export function getChunkNameFromMetafile(metafile: Metafile, file: string): string | undefined { - if (metafile.outputs[file]?.entryPoint) { - return getEntryPointName(metafile.outputs[file].entryPoint); +export function getChunkNameFromMetafile( + metafiles: Metafile[] | Metafile, + file: string, +): string | undefined { + const metafileArray = Array.isArray(metafiles) ? metafiles : [metafiles]; + for (const metafile of metafileArray) { + if (metafile.outputs[file]?.entryPoint) { + return getEntryPointName(metafile.outputs[file].entryPoint); + } } } @@ -214,7 +218,7 @@ export async function emitFilesToDisk( writeFileCallback: (file: T) => Promise, ): Promise { // Write files in groups of MAX_CONCURRENT_WRITES to avoid too many open files - for (let fileIndex = 0; fileIndex < files.length; ) { + for (let fileIndex = 0; fileIndex < files.length;) { const groupMax = Math.min(fileIndex + MAX_CONCURRENT_WRITES, files.length); const actions = []; @@ -226,71 +230,6 @@ export async function emitFilesToDisk( } } -/** - * Transform browserlists result to esbuild target. - * @see https://esbuild.github.io/api/#target - */ -export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] { - const transformed: string[] = []; - - // https://esbuild.github.io/api/#target - const esBuildSupportedBrowsers = new Set([ - 'chrome', - 'edge', - 'firefox', - 'ie', - 'ios', - 'node', - 'opera', - 'safari', - ]); - - for (const browser of supportedBrowsers) { - let [browserName, version] = browser.toLowerCase().split(' '); - - // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` - if (browserName === 'ios_saf') { - browserName = 'ios'; - } - - // browserslist uses ranges `15.2-15.3` versions but only the lowest is required - // to perform minimum supported feature checks. esbuild also expects a single version. - [version] = version.split('-'); - - if (esBuildSupportedBrowsers.has(browserName)) { - if (browserName === 'safari' && version === 'tp') { - // esbuild only supports numeric versions so `TP` is converted to a high number (999) since - // a Technology Preview (TP) of Safari is assumed to support all currently known features. - version = '999'; - } else if (!version.includes('.')) { - // A lone major version is considered by esbuild to include all minor versions. However, - // browserslist does not and is also inconsistent in its `.0` version naming. For example, - // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`. - version += '.0'; - } - - transformed.push(browserName + version); - } - } - - return transformed; -} - -const SUPPORTED_NODE_VERSIONS = '0.0.0-ENGINES-NODE'; - -/** - * Transform supported Node.js versions to esbuild target. - * @see https://esbuild.github.io/api/#target - */ -export function getSupportedNodeTargets(): string[] { - if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') { - // Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions. - return []; - } - - return SUPPORTED_NODE_VERSIONS.split('||').map((v) => 'node' + coerce(v)?.version); -} - interface BuildManifest { errors: string[]; warnings: string[]; diff --git a/packages/angular/build/src/tools/esbuild/watcher.ts b/packages/angular/build/src/tools/esbuild/watcher.ts index cf9e1d94cb87..b6e26f5c72af 100644 --- a/packages/angular/build/src/tools/esbuild/watcher.ts +++ b/packages/angular/build/src/tools/esbuild/watcher.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import WatchPack from 'watchpack'; +import type * as ParcelWatcher from '@parcel/watcher'; +import type * as Chokidar from 'chokidar'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import picomatch from 'picomatch'; +import { toPosixPath } from '../../utils/path'; export class ChangedFiles { readonly added = new Set(); @@ -14,7 +19,7 @@ export class ChangedFiles { readonly removed = new Set(); get all(): string[] { - return [...this.added, ...this.modified, ...this.removed]; + return Array.from(new Set([...this.added, ...this.modified, ...this.removed])); } toDebugString(): string { @@ -34,102 +39,581 @@ export interface BuildWatcher extends AsyncIterableIterator { close(): Promise; } -export function createWatcher(options?: { +export interface WatcherOptions { polling?: boolean; interval?: number; ignored?: string[]; followSymlinks?: boolean; -}): BuildWatcher { - const watcher = new WatchPack({ - poll: options?.polling ? (options?.interval ?? true) : false, - ignored: options?.ignored, - followSymlinks: options?.followSymlinks, - aggregateTimeout: 250, - }); - const watchedFiles = new Set(); + cwd?: string; +} + +/** + * Probes the filesystem at the specified target directory to determine whether it is case-sensitive. + */ +function isFileSystemCaseSensitive(targetDir: string = process.cwd()): boolean { + try { + const resolved = path.resolve(targetDir); + if (!fs.existsSync(resolved)) { + return process.platform !== 'win32' && process.platform !== 'darwin'; + } + + // Invert the casing of the target directory path. + const altCase = + resolved === resolved.toLowerCase() ? resolved.toUpperCase() : resolved.toLowerCase(); + + // If the path contains no alphabetic characters (e.g. root '/'), invert-casing + // produces the exact same string. Fall back to platform-specific defaults in this case. + if (resolved === altCase) { + return process.platform !== 'win32' && process.platform !== 'darwin'; + } + + // If both the original path and the inverted-casing path exist on disk, + // the filesystem is case-insensitive (returns false). + return !fs.existsSync(altCase); + } catch { + // If an error occurs (e.g., permission denied), default to the platform-specific + // behavior (case-insensitive on Windows/macOS, sensitive on Linux/Unix). + return process.platform !== 'win32' && process.platform !== 'darwin'; + } +} + +/** + * Normalizes a file system path string to POSIX format (forward slashes '/') + * and strips trailing slashes (except root '/' or Windows drive root 'C:/'). + */ +export function toPosixPathNormalized(pathString: string): string { + let posixPath = toPosixPath(pathString); + if (posixPath.length > 1 && posixPath.endsWith('/') && !/^[a-zA-Z]:\/$/.test(posixPath)) { + posixPath = posixPath.slice(0, -1); + } + + return posixPath; +} + +/** + * Returns a lookup key for set lookups and matching, lowercasing on case-insensitive file systems. + */ +function toLookupKey(posixPath: string, isCaseSensitive: boolean): string { + return isCaseSensitive ? posixPath : posixPath.toLowerCase(); +} - const nextQueue: ((value?: ChangedFiles) => void)[] = []; - let currentChangedFiles: ChangedFiles | undefined; +/** + * Returns the parent directory of a normalized POSIX path, correctly handling Windows drive roots. + */ +export function getDirectoryPath(posixPath: string): string { + const lastSlash = posixPath.lastIndexOf('/'); + if (lastSlash === -1) { + return '.'; + } + const dir = posixPath.slice(0, lastSlash); + if (dir === '' || dir.endsWith(':')) { + return dir + '/'; + } + + return dir; +} + +/** + * Determines whether a file path lookup key or any of its parent directories are present in watchedFiles. + */ +function isPathWatched(fileLookupKey: string, watchedFiles: Set): boolean { + if (watchedFiles.has(fileLookupKey)) { + return true; + } + + let current = fileLookupKey; + while (true) { + const parent = getDirectoryPath(current); + if (parent === current) { + break; + } + if (watchedFiles.has(parent)) { + return true; + } + current = parent; + } + + return false; +} + +class WatcherQueue { + private readonly nextQueue: ((value?: ChangedFiles) => void)[] = []; + private currentChangedFiles: ChangedFiles | undefined; + private isClosed = false; + private timeoutId: NodeJS.Timeout | undefined; + + addChange(type: 'added' | 'modified' | 'removed', file: string): void { + if (this.isClosed) { + return; + } + + const changedFiles = (this.currentChangedFiles ??= new ChangedFiles()); + changedFiles[type].add(file); + this.scheduleFlush(); + } - watcher.on('aggregated', (changes, removals) => { - const changedFiles = currentChangedFiles ?? new ChangedFiles(); - for (const file of changes) { - changedFiles.modified.add(file); + addChanges( + changes: ReadonlyArray<{ type: 'added' | 'modified' | 'removed'; file: string }>, + ): void { + if (this.isClosed || changes.length === 0) { + return; } - for (const file of removals) { - changedFiles.removed.add(file); + + const changedFiles = (this.currentChangedFiles ??= new ChangedFiles()); + for (const { type, file } of changes) { + changedFiles[type].add(file); } + this.scheduleFlush(); + } - const next = nextQueue.shift(); - if (next) { - currentChangedFiles = undefined; - next(changedFiles); - } else { - currentChangedFiles = changedFiles; + private scheduleFlush(): void { + if (this.timeoutId) { + clearTimeout(this.timeoutId); } - }); + this.timeoutId = setTimeout(() => { + this.timeoutId = undefined; + this.flush(); + }, 250); + } + + private flush(): void { + if ( + this.currentChangedFiles && + this.currentChangedFiles.all.length > 0 && + this.nextQueue.length > 0 + ) { + const next = this.nextQueue.shift(); + if (next) { + const result = this.currentChangedFiles; + this.currentChangedFiles = undefined; + next(result); + } + } + } + + async next(): Promise> { + if ( + this.currentChangedFiles && + this.currentChangedFiles.all.length > 0 && + this.nextQueue.length === 0 && + !this.timeoutId + ) { + const result = { value: this.currentChangedFiles }; + this.currentChangedFiles = undefined; + + return result; + } + + if (this.isClosed) { + return { done: true, value: undefined as unknown as ChangedFiles }; + } + + return new Promise((resolve) => { + this.nextQueue.push((value) => + resolve(value ? { value } : { done: true, value: undefined as unknown as ChangedFiles }), + ); + }); + } + + close(): void { + if (this.isClosed) { + return; + } + + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + } + + this.isClosed = true; + this.currentChangedFiles = undefined; + + let next; + while ((next = this.nextQueue.shift()) !== undefined) { + next(); + } + } +} + +export async function createWatcher(options?: WatcherOptions): Promise { + if (options?.polling) { + return createChokidarWatcher(options); + } + + try { + const parcelWatcher = await import('@parcel/watcher'); + + return await createParcelWatcher(options, parcelWatcher); + } catch { + return createChokidarWatcher(options); + } +} + +/** + * Checks whether a file path is located inside a parent directory. + * + * Input Expectations: + * - Both `file` and `dir` must be normalized POSIX-style paths (using forward slashes '/'). + * - Both paths must share the same casing normalization (e.g., lowercased on case-insensitive file systems). + */ +export function isPathInside(file: string, dir: string): boolean { + if (file === dir) { + return false; + } + + const dirWithSlash = dir.endsWith('/') ? dir : dir + '/'; + + return file.startsWith(dirWithSlash); +} + +class ParcelExternalManager { + private readonly extraSubscriptions = new Map(); + private readonly pendingSubscriptions = new Map< + string, + Promise + >(); + private readonly externalDirFiles = new Map }>(); + + constructor( + private readonly parcelWatcher: typeof ParcelWatcher, + private readonly options: WatcherOptions | undefined, + private readonly rootDirLookupKey: string, + private readonly handleEvents: (events: ParcelWatcher.Event[]) => void, + ) {} + + async ensureWatched(posixPath: string, lookupKey: string): Promise { + if (isPathInside(lookupKey, this.rootDirLookupKey) || lookupKey === this.rootDirLookupKey) { + return; + } + + const dirPath = getDirectoryPath(posixPath); + const dirKey = getDirectoryPath(lookupKey); + let dirEntry = this.externalDirFiles.get(dirKey); + if (!dirEntry) { + dirEntry = { dirPath, files: new Set() }; + this.externalDirFiles.set(dirKey, dirEntry); + } + dirEntry.files.add(lookupKey); + + await this.ensureDirWatched(dirPath, dirKey); + } + + removeFile(lookupKey: string): void { + if (isPathInside(lookupKey, this.rootDirLookupKey) || lookupKey === this.rootDirLookupKey) { + return; + } + + const dirKey = getDirectoryPath(lookupKey); + const dirEntry = this.externalDirFiles.get(dirKey); + if (dirEntry) { + dirEntry.files.delete(lookupKey); + if (dirEntry.files.size === 0) { + this.externalDirFiles.delete(dirKey); + const sub = this.extraSubscriptions.get(dirKey); + if (sub) { + this.extraSubscriptions.delete(dirKey); + sub.unsubscribe().catch(() => {}); + + for (const [remainingDirKey, remainingDirEntry] of this.externalDirFiles.entries()) { + if (!this.isCoveredByExistingExternal(remainingDirKey)) { + this.ensureDirWatched(remainingDirEntry.dirPath, remainingDirKey).catch(() => {}); + } + } + } + } + } + } + + async close(): Promise { + try { + if (this.pendingSubscriptions.size > 0) { + await Promise.allSettled(Array.from(this.pendingSubscriptions.values())); + } + if (this.extraSubscriptions.size > 0) { + await Promise.allSettled( + Array.from(this.extraSubscriptions.values()).map((sub) => sub.unsubscribe()), + ); + } + } finally { + this.extraSubscriptions.clear(); + this.pendingSubscriptions.clear(); + this.externalDirFiles.clear(); + } + } + + private isCoveredByExistingExternal(dirLookupKey: string): boolean { + for (const existingDir of this.extraSubscriptions.keys()) { + if (dirLookupKey === existingDir || isPathInside(dirLookupKey, existingDir)) { + return true; + } + } + for (const pendingDir of this.pendingSubscriptions.keys()) { + if (dirLookupKey === pendingDir || isPathInside(dirLookupKey, pendingDir)) { + return true; + } + } + + return false; + } + + private async ensureDirWatched(dirPath: string, dirKey: string): Promise { + if (this.isCoveredByExistingExternal(dirKey)) { + return; + } + + const subPromise = this.parcelWatcher.subscribe( + dirPath, + (err, events) => { + if (!err) { + this.handleEvents(events); + } + }, + { + ignore: this.options?.ignored, + }, + ); + + this.pendingSubscriptions.set(dirKey, subPromise); + + try { + const sub = await subPromise; + if (this.externalDirFiles.has(dirKey) && !this.isCoveredByExistingExternal(dirKey)) { + this.extraSubscriptions.set(dirKey, sub); + + // Subsume any nested child subscriptions that are now covered by this parent subscription + for (const [childDir, childSub] of this.extraSubscriptions.entries()) { + if (childDir !== dirKey && isPathInside(childDir, dirKey)) { + this.extraSubscriptions.delete(childDir); + childSub.unsubscribe().catch(() => {}); + } + } + } else { + sub.unsubscribe().catch(() => {}); + } + } catch { + // Ignore subscription errors for missing or restricted external directories + } finally { + this.pendingSubscriptions.delete(dirKey); + } + } +} + +async function createParcelWatcher( + options: WatcherOptions | undefined, + parcelWatcher: typeof ParcelWatcher, +): Promise { + const watchedFiles = new Set(); + const queue = new WatcherQueue(); + + const isCaseSensitive = isFileSystemCaseSensitive(options?.cwd); + const rootDirPosix = toPosixPathNormalized(options?.cwd ?? process.cwd()); + const rootDirLookupKey = toLookupKey(rootDirPosix, isCaseSensitive); + const initTime = Date.now(); + + const handleEvents = (events: ParcelWatcher.Event[]) => { + const changes: { type: 'added' | 'modified' | 'removed'; file: string }[] = []; + for (const event of events) { + const posixPath = toPosixPathNormalized(event.path); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + if (!isPathWatched(lookupKey, watchedFiles)) { + continue; + } + + if (event.type !== 'delete') { + const stat = fs.statSync(event.path, { throwIfNoEntry: false }); + // Ignore historical events from before watcher initialization, but allow a 1000 ms window + // to account for coarse filesystem timestamp resolution (e.g., ext4/overlayfs integer second + // mtime truncation on Linux) where files modified during startup may have truncated .000 ms mtimes. + if (stat && stat.mtimeMs < initTime - 1000) { + continue; + } + } + + const type = + event.type === 'create' ? 'added' : event.type === 'delete' ? 'removed' : 'modified'; + changes.push({ type, file: event.path }); + } + + if (changes.length > 0) { + queue.addChanges(changes); + } + }; - return { + const subscription = await parcelWatcher.subscribe( + rootDirPosix, + (err, events) => { + if (!err) { + handleEvents(events); + } + }, + { + ignore: options?.ignored, + }, + ); + + const externalManager = new ParcelExternalManager( + parcelWatcher, + options, + rootDirLookupKey, + handleEvents, + ); + + const buildWatcher: BuildWatcher = { [Symbol.asyncIterator]() { return this; }, - async next() { - if (currentChangedFiles && nextQueue.length === 0) { - const result = { value: currentChangedFiles }; - currentChangedFiles = undefined; + next() { + return queue.next(); + }, - return result; + add(paths) { + const targets = typeof paths === 'string' ? [paths] : paths; + for (const file of targets) { + const posixPath = toPosixPathNormalized(file); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + if (!watchedFiles.has(lookupKey)) { + watchedFiles.add(lookupKey); + void externalManager.ensureWatched(posixPath, lookupKey); + } } + }, - return new Promise((resolve) => { - nextQueue.push((value) => resolve(value ? { value } : { done: true, value })); - }); + remove(paths) { + const targets = typeof paths === 'string' ? [paths] : paths; + for (const file of targets) { + const posixPath = toPosixPathNormalized(file); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + if (watchedFiles.delete(lookupKey)) { + externalManager.removeFile(lookupKey); + } + } }, - add(paths) { - const previousSize = watchedFiles.size; - if (typeof paths === 'string') { - watchedFiles.add(paths); - } else { - for (const file of paths) { - watchedFiles.add(file); + async close() { + try { + if (subscription) { + await subscription.unsubscribe(); } + await externalManager.close(); + } finally { + queue.close(); } + }, + }; + + return buildWatcher; +} + +async function createChokidarWatcher( + options?: WatcherOptions, + chokidarModule?: typeof Chokidar, +): Promise { + const chokidar = chokidarModule ?? (await import('chokidar')); + const watchedFiles = new Set(); + const queue = new WatcherQueue(); + + const rootDir = options?.cwd ?? process.cwd(); + const isCaseSensitive = isFileSystemCaseSensitive(rootDir); + const rootDirPosix = toPosixPathNormalized(rootDir); + const rootDirLookupKey = toLookupKey(rootDirPosix, isCaseSensitive); - if (previousSize !== watchedFiles.size) { - watcher.watch({ - files: watchedFiles, - }); + const ignored = options?.ignored?.map((pattern) => { + if (/[*?[\]{}()]/.test(pattern)) { + const isMatch = picomatch(pattern, { dot: true }); + + return (filePath: string) => isMatch(toPosixPathNormalized(filePath)); + } + + return { path: toPosixPathNormalized(pattern), recursive: true }; + }); + + const watcher = chokidar.watch(rootDir, { + ignoreInitial: true, + ignored, + followSymlinks: options?.followSymlinks, + usePolling: !!options?.polling, + interval: options?.interval, + }); + const initTime = Date.now(); + + const handleEvent = (type: 'added' | 'modified' | 'removed', rawPath: string) => { + const posixPath = toPosixPathNormalized(rawPath); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + if (!isPathWatched(lookupKey, watchedFiles)) { + return; + } + + if (type !== 'removed') { + const stat = fs.statSync(rawPath, { throwIfNoEntry: false }); + // Ignore historical events from before watcher initialization, but allow a 1000 ms window + // to account for coarse filesystem timestamp resolution (e.g., ext4/overlayfs integer second + // mtime truncation on Linux) where files modified during startup may have truncated .000 ms mtimes. + if (stat && stat.mtimeMs < initTime - 1000) { + return; } + } + + queue.addChange(type, rawPath); + }; + + watcher.on('add', (path) => handleEvent('added', path)); + watcher.on('change', (path) => handleEvent('modified', path)); + watcher.on('unlink', (path) => handleEvent('removed', path)); + + const buildWatcher: BuildWatcher = { + [Symbol.asyncIterator]() { + return this; }, - remove(paths) { - const previousSize = watchedFiles.size; - if (typeof paths === 'string') { - watchedFiles.delete(paths); - } else { - for (const file of paths) { - watchedFiles.delete(file); + next() { + return queue.next(); + }, + + add(paths) { + const targets = typeof paths === 'string' ? [paths] : paths; + const newPaths: string[] = []; + for (const p of targets) { + const posixPath = toPosixPathNormalized(p); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + if (!watchedFiles.has(lookupKey)) { + watchedFiles.add(lookupKey); + if (!isPathInside(lookupKey, rootDirLookupKey) && lookupKey !== rootDirLookupKey) { + newPaths.push(posixPath); + } } } + if (newPaths.length > 0) { + watcher.add(newPaths); + } + }, - if (previousSize !== watchedFiles.size) { - watcher.watch({ - files: watchedFiles, - }); + remove(paths) { + const targets = typeof paths === 'string' ? [paths] : paths; + const removePaths: string[] = []; + for (const p of targets) { + const posixPath = toPosixPathNormalized(p); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + if (watchedFiles.has(lookupKey)) { + watchedFiles.delete(lookupKey); + if (!isPathInside(lookupKey, rootDirLookupKey) && lookupKey !== rootDirLookupKey) { + removePaths.push(posixPath); + } + } + } + if (removePaths.length > 0) { + watcher.unwatch(removePaths); } }, async close() { try { - watcher.close(); + await watcher.close(); } finally { - let next; - while ((next = nextQueue.shift()) !== undefined) { - next(); - } + queue.close(); } }, }; + + return buildWatcher; } diff --git a/packages/angular/build/src/tools/esbuild/watcher_spec.ts b/packages/angular/build/src/tools/esbuild/watcher_spec.ts new file mode 100644 index 000000000000..2c82510a5ae5 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/watcher_spec.ts @@ -0,0 +1,486 @@ +/** + * @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'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { setTimeout } from 'node:timers/promises'; +import { + ChangedFiles, + createWatcher, + getDirectoryPath, + isPathInside, + toPosixPathNormalized, +} from './watcher'; + +describe('Watcher', () => { + describe('toPosixPathNormalized', () => { + it('should strip trailing slashes for standard directories', () => { + expect(toPosixPathNormalized('/src/app/')).toBe('/src/app'); + expect(toPosixPathNormalized('C:/src/app/')).toBe('C:/src/app'); + }); + + it('should preserve single root slash', () => { + expect(toPosixPathNormalized('/')).toBe('/'); + }); + + it('should preserve trailing slash for Windows drive root', () => { + expect(toPosixPathNormalized('C:/')).toBe('C:/'); + expect(toPosixPathNormalized('c:/')).toBe('c:/'); + }); + }); + + describe('getDirectoryPath', () => { + it('should return parent directory for POSIX paths', () => { + expect(getDirectoryPath('/src/app/main.ts')).toBe('/src/app'); + expect(getDirectoryPath('/src/app')).toBe('/src'); + expect(getDirectoryPath('/src')).toBe('/'); + expect(getDirectoryPath('/')).toBe('/'); + }); + + it('should correctly handle Windows drive roots', () => { + expect(getDirectoryPath('C:/src/app/main.ts')).toBe('C:/src/app'); + expect(getDirectoryPath('C:/src')).toBe('C:/'); + expect(getDirectoryPath('C:/')).toBe('C:/'); + expect(getDirectoryPath('c:/')).toBe('c:/'); + }); + + it('should return dot for relative paths without slash', () => { + expect(getDirectoryPath('main.ts')).toBe('.'); + }); + }); + + describe('isPathInside', () => { + it('should return true for a file inside a directory', () => { + expect(isPathInside('/src/app/main.ts', '/src/app')).toBeTrue(); + }); + + it('should return false when file and dir are identical', () => { + expect(isPathInside('/src/app', '/src/app')).toBeFalse(); + }); + + it('should return false for sibling directories with matching prefix', () => { + expect(isPathInside('/src/app-other/main.ts', '/src/app')).toBeFalse(); + }); + + it('should handle Windows drive letters on the same drive', () => { + expect(isPathInside('c:/src/app/main.ts', 'c:/src/app')).toBeTrue(); + }); + + it('should return false for Windows drive letters on different drives', () => { + expect(isPathInside('d:/src/app/main.ts', 'c:/src/app')).toBeFalse(); + }); + + it('should handle root directory correctly', () => { + expect(isPathInside('/src/main.ts', '/')).toBeTrue(); + }); + + it('should handle Windows drive root directory correctly', () => { + expect(isPathInside('c:/src/main.ts', 'c:/')).toBeTrue(); + }); + }); + + describe('ChangedFiles', () => { + it('should track added, modified, and removed files', () => { + const changes = new ChangedFiles(); + changes.added.add('/src/app.component.ts'); + changes.modified.add('/src/main.ts'); + changes.removed.add('/src/old.ts'); + + expect(changes.all).toEqual(['/src/app.component.ts', '/src/main.ts', '/src/old.ts']); + }); + + it('should deduplicate files present in multiple sets in .all', () => { + const changes = new ChangedFiles(); + changes.added.add('/src/main.ts'); + changes.modified.add('/src/main.ts'); + + expect(changes.all).toEqual(['/src/main.ts']); + }); + + it('should format debug string correctly', () => { + const changes = new ChangedFiles(); + changes.modified.add('/src/main.ts'); + + const debug = JSON.parse(changes.toDebugString()); + expect(debug).toEqual({ + added: [], + modified: ['/src/main.ts'], + removed: [], + }); + }); + }); + + describe('createWatcher', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'watcher-spec-'))); + }); + + afterEach(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it('should instantiate and close watcher without error', async () => { + const watcher = await createWatcher({ cwd: tempDir }); + expect(watcher).toBeDefined(); + + watcher.add(path.join(tempDir, 'main.ts')); + watcher.remove(path.join(tempDir, 'main.ts')); + + await watcher.close(); + }); + + it('should support array of paths in add and remove', async () => { + const watcher = await createWatcher({ cwd: tempDir }); + const file1 = path.join(tempDir, 'a.ts'); + const file2 = path.join(tempDir, 'b.ts'); + + watcher.add([file1, file2]); + watcher.remove([file1, file2]); + + await watcher.close(); + }); + + it('should support polling option', async () => { + const watcher = await createWatcher({ polling: true, interval: 100, cwd: tempDir }); + expect(watcher).toBeDefined(); + + watcher.add(path.join(tempDir, 'main.ts')); + await watcher.close(); + }); + + it('should emit changes when a watched file is modified (chokidar polling)', async () => { + const testFile = path.join(tempDir, 'test.txt'); + fs.writeFileSync(testFile, 'initial'); + + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add(testFile); + + // Wait a short moment for watcher setup and mtime tick + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + // Trigger change + fs.writeFileSync(testFile, 'updated'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.length).toBeGreaterThan(0); + + await watcher.close(); + }, 10000); + + it('should preserve original path character casing in emitted changes', async () => { + const casedFile = path.join(tempDir, 'App.Component.ts'); + fs.writeFileSync(casedFile, 'initial'); + + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add(casedFile); + + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + fs.writeFileSync(casedFile, 'updated'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + const emittedFiles = result.value?.all ?? []; + expect(emittedFiles.some((f: string) => f.includes('App.Component.ts'))).toBeTrue(); + + await watcher.close(); + }, 10000); + + it('should emit changes when watching a directory containing modified files', async () => { + const subDir = path.join(tempDir, 'sub'); + fs.mkdirSync(subDir); + const testFile = path.join(subDir, 'nested.txt'); + fs.writeFileSync(testFile, 'initial'); + + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add(subDir); + + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + fs.writeFileSync(testFile, 'updated'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.length).toBeGreaterThan(0); + + await watcher.close(); + }, 10000); + + it('should support watching paths outside cwd', async () => { + const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'external-watcher-spec-')); + const externalFile = path.join(externalDir, 'external.txt'); + fs.writeFileSync(externalFile, 'initial'); + + try { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add(externalFile); + + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + fs.writeFileSync(externalFile, 'updated'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.some((f: string) => f.includes('external.txt'))).toBeTrue(); + + await watcher.close(); + } finally { + fs.rmSync(externalDir, { recursive: true, force: true }); + } + }, 10000); + + it('should handle adding multiple external files in the same directory concurrently', async () => { + const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'external-watcher-spec-')); + const file1 = path.join(externalDir, 'file1.txt'); + const file2 = path.join(externalDir, 'file2.txt'); + fs.writeFileSync(file1, 'initial1'); + fs.writeFileSync(file2, 'initial2'); + + try { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add([file1, file2]); + + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + let nextPromise = iterator.next(); + fs.writeFileSync(file1, 'updated1'); + let result = await nextPromise; + expect(result.value?.all.some((f: string) => f.includes('file1.txt'))).toBeTrue(); + + nextPromise = iterator.next(); + fs.writeFileSync(file2, 'updated2'); + result = await nextPromise; + expect(result.value?.all.some((f: string) => f.includes('file2.txt'))).toBeTrue(); + + await watcher.close(); + } finally { + fs.rmSync(externalDir, { recursive: true, force: true }); + } + }, 10000); + + it('should clean up external subscriptions when all external files in a directory are removed', async () => { + const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'external-watcher-spec-')); + const file1 = path.join(externalDir, 'file1.txt'); + const file2 = path.join(externalDir, 'file2.txt'); + fs.writeFileSync(file1, 'initial1'); + fs.writeFileSync(file2, 'initial2'); + + try { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add([file1, file2]); + + await setTimeout(100); + + // Remove files from watcher + watcher.remove(file1); + watcher.remove(file2); + + await watcher.close(); + } finally { + fs.rmSync(externalDir, { recursive: true, force: true }); + } + }); + + it('should handle nested external directories without creating duplicate subscriptions', async () => { + const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'external-watcher-spec-')); + const subDir = path.join(externalDir, 'sub'); + fs.mkdirSync(subDir); + const parentFile = path.join(externalDir, 'parent.txt'); + const childFile = path.join(subDir, 'child.txt'); + fs.writeFileSync(parentFile, 'initial-parent'); + fs.writeFileSync(childFile, 'initial-child'); + + try { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add(parentFile); + watcher.add(childFile); + + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + fs.writeFileSync(childFile, 'updated-child'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.some((f: string) => f.includes('child.txt'))).toBeTrue(); + + await watcher.close(); + } finally { + fs.rmSync(externalDir, { recursive: true, force: true }); + } + }, 10000); + + it('should subscribe to subsumed external child directory when parent external subscription is removed', async () => { + const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'external-watcher-spec-')); + const subDir = path.join(externalDir, 'sub'); + fs.mkdirSync(subDir); + const parentFile = path.join(externalDir, 'parent.txt'); + const childFile = path.join(subDir, 'child.txt'); + fs.writeFileSync(parentFile, 'initial-parent'); + fs.writeFileSync(childFile, 'initial-child'); + + try { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + watcher.add(parentFile); + watcher.add(childFile); + + await setTimeout(100); + + watcher.remove(parentFile); + + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + fs.writeFileSync(childFile, 'updated-child'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.some((f: string) => f.includes('child.txt'))).toBeTrue(); + + await watcher.close(); + } finally { + fs.rmSync(externalDir, { recursive: true, force: true }); + } + }, 10000); + + it('should signal completion on close', async () => { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + const iterator = watcher[Symbol.asyncIterator](); + + const nextPromise = iterator.next(); + await watcher.close(); + + const result = await nextPromise; + expect(result.done).toBeTrue(); + }); + + it('should return done immediately if next() is called after close()', async () => { + const watcher = await createWatcher({ polling: true, interval: 50, cwd: tempDir }); + await watcher.close(); + + const result = await watcher.next(); + expect(result.done).toBeTrue(); + }); + + it('should ignore stale modifications before initTime and emit changes after initTime (@parcel/watcher)', async () => { + const testFile = path.join(tempDir, 'test.txt'); + fs.writeFileSync(testFile, 'initial'); + + // Small delay to ensure initial mtimeMs is strictly earlier than initTime - 1000 + await setTimeout(1100); + + // Create native @parcel/watcher (polling: false / default) + const watcher = await createWatcher({ cwd: tempDir }); + watcher.add(testFile); + + // Wait a short moment for native watcher setup and kernel event stream initialization + await setTimeout(150); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + // Trigger a change after watcher initialization + fs.writeFileSync(testFile, 'updated'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + expect(result.value?.all.some((f: string) => f.includes('test.txt'))).toBeTrue(); + + await watcher.close(); + }, 10000); + + it('should emit changes when a file is deleted and recreated with stabilization delay (@parcel/watcher)', async () => { + const testFile = path.join(tempDir, 'recreate.txt'); + fs.writeFileSync(testFile, 'initial'); + + await setTimeout(50); + + const watcher = await createWatcher({ cwd: tempDir }); + watcher.add(testFile); + + await setTimeout(150); + + const iterator = watcher[Symbol.asyncIterator](); + + // Delete the file + fs.rmSync(testFile); + let result = await iterator.next(); + expect(result.done).toBeFalsy(); + expect(result.value?.removed.size).toBeGreaterThan(0); + + // Brief stabilization delay before recreating to prevent macOS fsevents kernel driver + // from coalescing unlink and create into a single directory event + await setTimeout(150); + + // Recreate the file + fs.writeFileSync(testFile, 'recreated'); + result = await iterator.next(); + expect(result.done).toBeFalsy(); + expect(result.value?.added.size).toBeGreaterThan(0); + + await watcher.close(); + }, 10000); + + it('should ignore changes matching glob patterns in polling mode (chokidar)', async () => { + const ignoredDir = path.join(tempDir, 'dist'); + fs.mkdirSync(ignoredDir); + const ignoredFile = path.join(ignoredDir, 'bundle.js'); + const watchedFile = path.join(tempDir, 'src.ts'); + fs.writeFileSync(ignoredFile, 'initial-dist'); + fs.writeFileSync(watchedFile, 'initial-src'); + + const watcher = await createWatcher({ + polling: true, + interval: 50, + cwd: tempDir, + ignored: [`${toPosixPathNormalized(ignoredDir)}/**`], + }); + + watcher.add(tempDir); + await setTimeout(100); + + const iterator = watcher[Symbol.asyncIterator](); + const nextPromise = iterator.next(); + + // Trigger changes in ignored file and watched file + fs.writeFileSync(ignoredFile, 'updated-dist'); + fs.writeFileSync(watchedFile, 'updated-src'); + + const result = await nextPromise; + expect(result.done).toBeFalsy(); + const emitted = result.value?.all ?? []; + expect(emitted.some((f: string) => f.includes('src.ts'))).toBeTrue(); + expect(emitted.some((f: string) => f.includes('bundle.js'))).toBeFalse(); + + await watcher.close(); + }, 10000); + }); +}); diff --git a/packages/angular/build/src/tools/oxc/adjust-static-class-members_oxc_spec.ts b/packages/angular/build/src/tools/oxc/adjust-static-class-members_oxc_spec.ts new file mode 100644 index 000000000000..7fe8c68030fc --- /dev/null +++ b/packages/angular/build/src/tools/oxc/adjust-static-class-members_oxc_spec.ts @@ -0,0 +1,1103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { transform } from './oxc-transform'; + +const NO_CHANGE = Symbol('NO_CHANGE'); + +function cleanCode(code: string): string { + return code.replace(/\s+/g, '').replace(/,([}\])])/g, '$1'); +} + +function testCase({ + input, + expected, + options, +}: { + input: string; + expected: string | typeof NO_CHANGE; + options?: { wrapDecorators?: boolean }; +}): jasmine.ImplementationCallback { + return async () => { + const result = transform('test.js', input, { + sourcemap: false, + sideEffects: options?.wrapDecorators ? false : true, + pureAnnotate: false, + }); + if (!result?.code) { + fail('Expected oxc-transform to return a transform result.'); + } else { + const actualClean = cleanCode(result.code); + const expectedClean = cleanCode(expected === NO_CHANGE ? input : expected); + expect(actualClean).toEqual(expectedClean); + } + }; +} + +describe('adjust-static-class-members oxc-transform implementation', () => { + it( + 'elides empty ctorParameters function expression static field', + testCase({ + input: ` + export class SomeClass {} + SomeClass.ctorParameters = function () { return []; }; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'elides non-empty ctorParameters function expression static field', + testCase({ + input: ` + export class SomeClass {} + SomeClass.ctorParameters = function () { return [{type: Injector}]; }; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'elides empty ctorParameters arrow expression static field', + testCase({ + input: ` + export class SomeClass {} + SomeClass.ctorParameters = () => []; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'elides non-empty ctorParameters arrow expression static field', + testCase({ + input: ` + export class SomeClass {} + SomeClass.ctorParameters = () => [{type: Injector}]; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'keeps ctorParameters static field without arrow/function expression', + testCase({ + input: ` + export class SomeClass {} + SomeClass.ctorParameters = 42; + `, + expected: ` + export let SomeClass = /*#__PURE__*/ (() => { + class SomeClass {} + SomeClass.ctorParameters = 42; + return SomeClass; + })(); + `, + }), + ); + + it( + 'elides empty decorators static field with array literal', + testCase({ + input: ` + export class SomeClass {} + SomeClass.decorators = []; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'elides non-empty decorators static field with array literal', + testCase({ + input: ` + export class SomeClass {} + SomeClass.decorators = [{ type: Injectable }]; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'keeps decorators static field without array literal', + testCase({ + input: ` + export class SomeClass {} + SomeClass.decorators = 42; + `, + expected: ` + export let SomeClass = /*#__PURE__*/ (() => { + class SomeClass {} + SomeClass.decorators = 42; + return SomeClass; + })(); + `, + }), + ); + + it( + 'elides empty propDecorators static field with object literal', + testCase({ + input: ` + export class SomeClass {} + SomeClass.propDecorators = {}; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'elides non-empty propDecorators static field with object literal', + testCase({ + input: ` + export class SomeClass {} + SomeClass.propDecorators = { 'ngIf': [{ type: Input }] }; + `, + expected: 'export class SomeClass {}', + }), + ); + + it( + 'keeps propDecorators static field without object literal', + testCase({ + input: ` + export class SomeClass {} + SomeClass.propDecorators = 42; + `, + expected: ` + export let SomeClass = /*#__PURE__*/ (() => { + class SomeClass {} + SomeClass.propDecorators = 42; + return SomeClass; + })(); + `, + }), + ); + + it( + 'does not wrap default exported class with no connected siblings', + testCase({ + // NOTE: This could technically have no changes but the default export splitting detection + // does not perform class property analysis currently. + input: ` + export default class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + `, + expected: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + export { CustomComponentEffects as default }; + `, + }), + ); + + it( + 'does wrap not default exported class with only side effect fields', + testCase({ + input: ` + export default class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = console.log('foo'); + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap class with only side effect fields', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = console.log('foo'); + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap class with only side effect native fields', + testCase({ + input: ` + class CustomComponentEffects { + static someFieldWithSideEffects = console.log('foo'); + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap class with only instance native fields', + testCase({ + input: ` + class CustomComponentEffects { + someFieldWithSideEffects = console.log('foo'); + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + `, + expected: NO_CHANGE, + }), + ); + + it( + 'wraps class with pure annotated side effect fields (#__PURE__)', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = /*#__PURE__*/ console.log('foo'); + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = /*#__PURE__*/ console.log('foo'); + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps class with pure annotated side effect native fields (#__PURE__)', + testCase({ + input: ` + class CustomComponentEffects { + static someFieldWithSideEffects = /*#__PURE__*/ console.log('foo'); + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + static someFieldWithSideEffects = /*#__PURE__*/ console.log('foo'); + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps class with pure annotated side effect fields (@__PURE__)', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = /*@__PURE__*/ console.log('foo'); + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = /*@__PURE__*/ console.log('foo'); + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps class with pure annotated side effect fields (@pureOrBreakMyCode)', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = /**@pureOrBreakMyCode*/ console.log('foo'); + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = + /**@pureOrBreakMyCode*/ console.log('foo'); + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps class with closure pure annotated side effect fields', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = /* @pureOrBreakMyCode */ console.log('foo'); + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = + /* @pureOrBreakMyCode */ console.log('foo'); + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps exported class with a pure static field', + testCase({ + input: ` + export class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + `, + expected: ` + export let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps exported class with a pure native static field', + testCase({ + input: ` + export class CustomComponentEffects { + static someField = 42; + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + `, + expected: ` + export let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + static someField = 42; + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps class with a basic literal static field', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + return CustomComponentEffects; + })(); + `, + }), + ); + + it( + 'wraps class with a pure static field', + testCase({ + input: ` + const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef; + const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__; + class TemplateRef {} + TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY; + `, + expected: ` + const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef; + const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__; + let TemplateRef = /*#__PURE__*/ (() => { + class TemplateRef {} + TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY; + return TemplateRef; + })(); + `, + }), + ); + + it( + 'wraps class with multiple pure static field', + testCase({ + input: ` + const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef; + const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__; + class TemplateRef {} + TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY; + TemplateRef.someField = 42; + `, + expected: ` + const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef; + const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__; + let TemplateRef = /*#__PURE__*/ (() => { + class TemplateRef {} + TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY; + TemplateRef.someField = 42; + return TemplateRef; + })(); + `, + }), + ); + + it( + 'does not wrap class with only some pure static fields', + testCase({ + input: ` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + CustomComponentEffects.someFieldWithSideEffects = console.log('foo'); + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap class with only pure native static fields and some side effect static fields', + testCase({ + input: ` + class CustomComponentEffects { + static someField = 42; + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someFieldWithSideEffects = console.log('foo'); + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap class with only some pure native static fields', + testCase({ + input: ` + class CustomComponentEffects { + static someField = 42; + static someFieldWithSideEffects = console.log('foo'); + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap class with class decorators when wrapDecorators is false', + testCase({ + input: ` + let SomeClass = class SomeClass { + }; + SomeClass = __decorate([ + Dec() + ], SomeClass); + `, + expected: NO_CHANGE, + options: { wrapDecorators: false }, + }), + ); + + it( + 'wraps class with Angular ɵfac static field (esbuild)', + testCase({ + input: ` + var Comp2Component = class _Comp2Component { + static { + this.ɵfac = function Comp2Component_Factory(t) { + return new (t || _Comp2Component)(); + }; + } + }; + `, + expected: ` + var Comp2Component = /*#__PURE__*/ (() => { + let Comp2Component = class _Comp2Component { + static { + this.ɵfac = function Comp2Component_Factory(t) { + return new (t || _Comp2Component)(); + }; + } + }; + return Comp2Component; + })(); + `, + }), + ); + + it( + 'wraps class with class decorators when wrapDecorators is true (esbuild output)', + testCase({ + input: ` + var ExampleClass = class { + method() { + } + }; + __decorate([ + SomeDecorator() + ], ExampleClass.prototype, "method", null); + `, + expected: ` + var ExampleClass = /*#__PURE__*/ (() => { + let ExampleClass = class { + method() {} + }; + __decorate([SomeDecorator()], ExampleClass.prototype, "method", null); + return ExampleClass; + })(); + `, + options: { wrapDecorators: true }, + }), + ); + + it( + 'wraps class with class decorators when wrapDecorators is true', + testCase({ + input: ` + let SomeClass = class SomeClass { + }; + SomeClass = __decorate([ + SomeDecorator() + ], SomeClass); + `, + expected: ` + let SomeClass = /*#__PURE__*/ (() => { + let SomeClass = class SomeClass { + }; + SomeClass = __decorate([ + SomeDecorator() + ], SomeClass); + return SomeClass; + })(); + `, + options: { wrapDecorators: true }, + }), + ); + + it( + 'does not wrap class with constructor decorators when wrapDecorators is false', + testCase({ + input: ` + let SomeClass = class SomeClass { + constructor(foo) { } + }; + SomeClass = __decorate([ + __param(0, SomeDecorator) + ], SomeClass); + `, + expected: NO_CHANGE, + options: { wrapDecorators: false }, + }), + ); + + it( + 'wraps class with constructor decorators when wrapDecorators is true', + testCase({ + input: ` + let SomeClass = class SomeClass { + constructor(foo) { } + }; + SomeClass = __decorate([ + __param(0, SomeDecorator) + ], SomeClass); + `, + expected: ` + let SomeClass = /*#__PURE__*/ (() => { + let SomeClass = class SomeClass { + constructor(foo) { } + }; + SomeClass = __decorate([ + __param(0, SomeDecorator) + ], SomeClass); + return SomeClass; + })(); + `, + options: { wrapDecorators: true }, + }), + ); + + it( + 'does not wrap class with field decorators when wrapDecorators is false', + testCase({ + input: ` + class SomeClass { + constructor() { + this.foo = 42; + } + } + __decorate([ + SomeDecorator + ], SomeClass.prototype, "foo", void 0); + `, + expected: NO_CHANGE, + options: { wrapDecorators: false }, + }), + ); + + it( + 'wraps class with field decorators when wrapDecorators is true', + testCase({ + input: ` + class SomeClass { + constructor() { + this.foo = 42; + } + } + __decorate([ + SomeDecorator + ], SomeClass.prototype, "foo", void 0); + `, + expected: ` + let SomeClass = /*#__PURE__*/ (() => { + class SomeClass { + constructor() { + this.foo = 42; + } + } + __decorate([ + SomeDecorator + ], SomeClass.prototype, "foo", void 0); + return SomeClass; + })(); + `, + options: { wrapDecorators: true }, + }), + ); + + it( + 'wraps class with Angular ɵfac static field', + testCase({ + input: ` + class CommonModule { + } + CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; + `, + expected: ` + let CommonModule = /*#__PURE__*/ (() => { + class CommonModule { + } + CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; + return CommonModule; + })(); + `, + }), + ); + + it( + 'wraps class with Angular ɵfac static block (ES2022 + useDefineForClassFields: false)', + testCase({ + input: ` + class CommonModule { + static { this.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; } + static { this.ɵmod = ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); } + } + `, + expected: ` + let CommonModule = /*#__PURE__*/ (() => { + class CommonModule { + static { + this.ɵfac = function CommonModule_Factory(t) { + return new (t || CommonModule)(); + }; + } + static { + this.ɵmod = ɵngcc0.ɵɵdefineNgModule({ + type: CommonModule, + }); + } + } + return CommonModule; + })(); + `, + }), + ); + + it( + 'does not wrap class with side effect full static block (ES2022 + useDefineForClassFields: false)', + testCase({ + input: ` + class CommonModule { + static { globalThis.bar = 1 } + } + `, + expected: NO_CHANGE, + }), + ); + + it( + 'wraps class with Angular ɵmod static field', + testCase({ + input: ` + class CommonModule { + } + CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); + `, + expected: ` + let CommonModule = /*#__PURE__*/ (() => { + class CommonModule { + } + CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); + return CommonModule; + })(); + `, + }), + ); + + it( + 'wraps class with Angular ɵinj static field', + testCase({ + input: ` + class CommonModule { + } + CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [ + { provide: NgLocalization, useClass: NgLocaleLocalization }, + ] }); + `, + expected: ` + let CommonModule = /*#__PURE__*/ (() => { + class CommonModule { + } + CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [ + { + provide: NgLocalization, + useClass: NgLocaleLocalization + }, + ] }); + return CommonModule; + })(); + `, + }), + ); + + it( + 'wraps class with multiple Angular static fields', + testCase({ + input: ` + class CommonModule { + } + CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; + CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); + CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [ + { provide: NgLocalization, useClass: NgLocaleLocalization }, + ] }); + `, + expected: ` + let CommonModule = /*#__PURE__*/ (() => { + class CommonModule { + } + CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; + CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); + CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [ + { + provide: NgLocalization, + useClass: NgLocaleLocalization + }, + ]}); + return CommonModule; + })(); + `, + }), + ); + + it( + 'wraps class with multiple Angular native static fields', + testCase({ + input: ` + class CommonModule { + static ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; + static ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); + static ɵinj = ɵngcc0.ɵɵdefineInjector({ providers: [ + { provide: NgLocalization, useClass: NgLocaleLocalization }, + ] }); + } + `, + expected: ` + let CommonModule = /*#__PURE__*/ (() => { + class CommonModule { + static ɵfac = function CommonModule_Factory(t) { + return new (t || CommonModule)(); + }; + static ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ + type: CommonModule, + }); + static ɵinj = ɵngcc0.ɵɵdefineInjector({ + providers: [ + { + provide: NgLocalization, + useClass: NgLocaleLocalization, + }, + ], + }); + } + return CommonModule; + })(); + `, + }), + ); + + it( + 'wraps default exported class with pure static fields', + testCase({ + input: ` + export default class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + `, + expected: ` + let CustomComponentEffects = /*#__PURE__*/ (() => { + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.someField = 42; + return CustomComponentEffects; + })(); + export { CustomComponentEffects as default }; + `, + }), + ); + it( + 'supports class with empty static block and wraps pure static properties', + testCase({ + input: ` + class MyClass { + static {} + static ɵprov = 42; + } + `, + expected: ` + let MyClass = /*#__PURE__*/ (() => { + class MyClass { + static {} + static ɵprov = 42; + } + return MyClass; + })(); + `, + }), + ); + + it( + 'does not wrap class with non-empty static block', + testCase({ + input: ` + class MyClass { + static { console.log(1); } + static ɵprov = 42; + } + `, + expected: ` + class MyClass { + static { console.log(1); } + static ɵprov = 42; + } + `, + }), + ); + + it( + 'wraps adjacent class declarations without interleaving', + testCase({ + input: 'class A{static s=[1]}class B{static s=[2]}', + expected: ` + let A = /*#__PURE__*/ (() => { + class A { + static s = [1] + } + return A; + })(); + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'wraps adjacent exported class declarations without interleaving', + testCase({ + input: 'export class A{static s=[1]}export class B{static s=[2]}', + expected: ` + export let A = /*#__PURE__*/ (() => { + class A { + static s = [1] + } + return A; + })(); + export let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'wraps adjacent default export and class declarations without interleaving', + testCase({ + input: 'export default class A{static s=[1]}class B{static s=[2]}', + expected: ` + let A = /*#__PURE__*/ (() => { + class A { + static s = [1] + } + return A; + })(); + export { A as default }; + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'splits default export without interleaving with adjacent class declaration', + testCase({ + input: 'export default class A{}class B{static s=[2]}', + expected: ` + class A {} + export { A as default }; + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'wraps adjacent variable class declarations without interleaving', + testCase({ + input: 'let A=class A{static s=[1]};class B{static s=[2]}', + expected: ` + let A = /*#__PURE__*/ (() => { + let A = class A { + static s = [1] + }; + return A; + })(); + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); +}); diff --git a/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts b/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts new file mode 100644 index 000000000000..82877cb3b2e3 --- /dev/null +++ b/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts @@ -0,0 +1,382 @@ +/** + * @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 { format } from 'prettier'; +import { transform } from './oxc-transform'; + +const NO_CHANGE = Symbol('NO_CHANGE'); + +function testCase({ + input, + expected, +}: { + input: string; + expected: string | typeof NO_CHANGE; +}): jasmine.ImplementationCallback { + return async () => { + const result = transform('test.js', input, { + sourcemap: false, + pureAnnotate: false, + }); + if (!result?.code) { + fail('Expected oxc-transform to return a transform result.'); + } else { + expect(await format(result.code, { parser: 'babel' })).toEqual( + await format(expected === NO_CHANGE ? input : expected, { parser: 'babel' }), + ); + } + }; +} + +describe('adjust-typescript-enums oxc-transform implementation', () => { + it( + 'wraps unexported TypeScript enums', + testCase({ + input: ` + var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); + `, + expected: ` + var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush"; + ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy || {}); + `, + }), + ); + + it( + 'wraps exported TypeScript enums', + testCase({ + input: ` + export var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); + `, + expected: ` + export var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush"; + ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy || {}); + `, + }), + ); + + it( + 'does not wrap exported TypeScript enums from CommonJS (<5.1)', + testCase({ + input: ` + var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy = exports.ChangeDetectionStrategy || (exports.ChangeDetectionStrategy = {})); + `, + expected: NO_CHANGE, + }), + ); + + it( + 'wraps exported TypeScript enums from CommonJS (5.1+)', + testCase({ + input: ` + var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (exports.ChangeDetectionStrategy = ChangeDetectionStrategy = {})); + `, + expected: ` + var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush"; + ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy || (exports.ChangeDetectionStrategy = ChangeDetectionStrategy = {})); + `, + }), + ); + + it( + 'wraps TypeScript enums with custom numbering', + testCase({ + input: ` + export var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 5] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 8] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); + `, + expected: ` + export var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 5)] = "OnPush"; + ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 8)] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy || {}); + `, + }), + ); + + it( + 'wraps string-based TypeScript enums', + testCase({ + input: ` + var NotificationKind; + (function (NotificationKind) { + NotificationKind["NEXT"] = "N"; + NotificationKind["ERROR"] = "E"; + NotificationKind["COMPLETE"] = "C"; + })(NotificationKind || (NotificationKind = {})); + `, + expected: ` + var NotificationKind = /*#__PURE__*/ (function (NotificationKind) { + NotificationKind["NEXT"] = "N"; + NotificationKind["ERROR"] = "E"; + NotificationKind["COMPLETE"] = "C"; + return NotificationKind; + })(NotificationKind || {}); + `, + }), + ); + + it( + 'wraps enums that were renamed due to scope hoisting', + testCase({ + input: ` + var NotificationKind$1; + (function (NotificationKind) { + NotificationKind["NEXT"] = "N"; + NotificationKind["ERROR"] = "E"; + NotificationKind["COMPLETE"] = "C"; + })(NotificationKind$1 || (NotificationKind$1 = {})); + `, + expected: ` + var NotificationKind$1 = /*#__PURE__*/ (function (NotificationKind) { + NotificationKind["NEXT"] = "N"; + NotificationKind["ERROR"] = "E"; + NotificationKind["COMPLETE"] = "C"; + return NotificationKind; + })(NotificationKind$1 || {}); + `, + }), + ); + + it( + 'maintains multi-line comments', + testCase({ + input: ` + /** + * Supported http methods. + * @deprecated use @angular/common/http instead + */ + var RequestMethod; + (function (RequestMethod) { + RequestMethod[RequestMethod["Get"] = 0] = "Get"; + RequestMethod[RequestMethod["Post"] = 1] = "Post"; + RequestMethod[RequestMethod["Put"] = 2] = "Put"; + RequestMethod[RequestMethod["Delete"] = 3] = "Delete"; + RequestMethod[RequestMethod["Options"] = 4] = "Options"; + RequestMethod[RequestMethod["Head"] = 5] = "Head"; + RequestMethod[RequestMethod["Patch"] = 6] = "Patch"; + })(RequestMethod || (RequestMethod = {})); + `, + expected: ` + /** + * Supported http methods. + * @deprecated use @angular/common/http instead + */ + var RequestMethod = /*#__PURE__*/ (function (RequestMethod) { + RequestMethod[(RequestMethod["Get"] = 0)] = "Get"; + RequestMethod[(RequestMethod["Post"] = 1)] = "Post"; + RequestMethod[(RequestMethod["Put"] = 2)] = "Put"; + RequestMethod[(RequestMethod["Delete"] = 3)] = "Delete"; + RequestMethod[(RequestMethod["Options"] = 4)] = "Options"; + RequestMethod[(RequestMethod["Head"] = 5)] = "Head"; + RequestMethod[(RequestMethod["Patch"] = 6)] = "Patch"; + return RequestMethod; + })(RequestMethod || {}); + `, + }), + ); + + it( + 'does not wrap TypeScript enums with side effect values', + testCase({ + input: ` + export var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = console.log('foo'); + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); + `, + expected: NO_CHANGE, + }), + ); + + it( + 'does not wrap object literals similar to TypeScript enums', + testCase({ + input: ` + const RendererStyleFlags3 = { + Important: 1, + DashCase: 2, + }; + if (typeof RendererStyleFlags3 === 'object') { + RendererStyleFlags3[RendererStyleFlags3.Important] = 'DashCase'; + } + RendererStyleFlags3[RendererStyleFlags3.Important] = 'Important'; + `, + expected: NO_CHANGE, + }), + ); + + it( + 'wraps TypeScript enums', + testCase({ + input: ` + var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); + `, + expected: ` + var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush"; + ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy || {}); + `, + }), + ); + + it( + 'should wrap TypeScript enums if the declaration identifier has been renamed to avoid collisions', + testCase({ + input: ` + var ChangeDetectionStrategy$1; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy$1 || (ChangeDetectionStrategy$1 = {})); + `, + expected: ` + var ChangeDetectionStrategy$1 = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy$1 || {}); + `, + }), + ); + + it( + 'handles TypeScript enums with chained exports assignment (angular-split / shared-docs pattern)', + testCase({ + input: ` + var Area; + (function (a1) { + a1[a1["areaAfter"] = 0] = "areaAfter"; + a1[a1["preserveOtherCategoryOrder"] = 1] = "preserveOtherCategoryOrder"; + })(Area || (Area = exports.Area = {})); + `, + expected: ` + var Area = /*#__PURE__*/ (function (a1) { + a1[(a1["areaAfter"] = 0)] = "areaAfter"; + a1[(a1["preserveOtherCategoryOrder"] = 1)] = "preserveOtherCategoryOrder"; + return a1; + })(Area || (exports.Area = {})); + `, + }), + ); + + it( + 'handles TypeScript enums wrapped in parentheses', + testCase({ + input: ` + var ChangeDetectionStrategy; + ((function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}))); + `, + expected: ` + var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush"; + ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default"; + return ChangeDetectionStrategy; + })(ChangeDetectionStrategy || {}); + `, + }), + ); + + it( + 'wraps Crockford-style TypeScript enum IIFE without leaving dangling parentheses', + testCase({ + input: ` + var HDirection; + (function (HDirection) { + HDirection[HDirection['Backwards'] = -1] = 'Backwards'; + HDirection[HDirection['Forwards'] = 1] = 'Forwards'; + }(HDirection || (HDirection = {}))); + const nextStatement = true; + `, + expected: ` + var HDirection = /*#__PURE__*/ (function (HDirection) { + HDirection[(HDirection['Backwards'] = -1)] = 'Backwards'; + HDirection[(HDirection['Forwards'] = 1)] = 'Forwards'; + return HDirection; + }(HDirection || {})); + + const nextStatement = true; + `, + }), + ); + + it( + 'wraps TypeScript enum IIFE with multiple nested parentheses', + testCase({ + input: ` + var Foo; + (((function (Foo) { + Foo[Foo['A'] = 0] = 'A'; + }(Foo || (Foo = {}))))); + `, + expected: ` + var Foo = /*#__PURE__*/ (function (Foo) { + Foo[(Foo['A'] = 0)] = 'A'; + return Foo; + }(Foo || {})); + `, + }), + ); + + it( + 'wraps Crockford-style TypeScript enum IIFE with chained export assignments', + testCase({ + input: ` + var Foo; + (function (Foo) { + Foo[Foo['A'] = 0] = 'A'; + }(Foo || (Foo = exports.Foo = {}))); + `, + expected: ` + var Foo = /*#__PURE__*/ (function (Foo) { + Foo[(Foo['A'] = 0)] = 'A'; + return Foo; + }(Foo || (exports.Foo = {}))); + `, + }), + ); +}); diff --git a/packages/angular/build/src/tools/oxc/elide-angular-metadata_oxc_spec.ts b/packages/angular/build/src/tools/oxc/elide-angular-metadata_oxc_spec.ts new file mode 100644 index 000000000000..f4d6c09b6c19 --- /dev/null +++ b/packages/angular/build/src/tools/oxc/elide-angular-metadata_oxc_spec.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { transform } from './oxc-transform'; + +function cleanCode(code: string): string { + return code + .replace(/\s+/g, '') + .replace(/"/g, "'") + .replace(/;}/g, '}') + .replace(/,([}\])])/g, '$1'); +} + +function testCase({ + input, + expected, +}: { + input: string; + expected: string; +}): jasmine.ImplementationCallback { + return async () => { + const result = transform('test.js', input, { + sourcemap: false, + pureAnnotate: false, + }); + if (!result?.code) { + fail('Expected oxc-transform to return a transform result.'); + } else { + const actualClean = cleanCode(result.code); + const expectedClean = cleanCode(expected); + expect(actualClean).toEqual(expectedClean); + } + }; +} + +describe('elide-angular-metadata oxc-transform implementation', () => { + it( + 'elides pure annotated ɵsetClassMetadata', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(Clazz, [{ + type: Component, + args: [{ + selector: 'app-lazy', + template: 'very lazy', + styles: [] + }] + }], null, null); })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (function () { void 0 })(); + `, + }), + ); + + it( + 'elides JIT mode protected ɵsetClassMetadata', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'app-lazy', + template: 'very lazy', + styles: [] + }] + }], null, null); })();`, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + (function () { (typeof ngJitMode === "undefined" || ngJitMode) && void 0 })();`, + }), + ); + + it( + 'elides ɵsetClassMetadata inside an arrow-function-based IIFE', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (() => { i0.ɵsetClassMetadata(Clazz, [{ + type: Component, + args: [{ + selector: 'app-lazy', + template: 'very lazy', + styles: [] + }] + }], null, null); })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (() => { void 0 })(); + `, + }), + ); + + it( + 'elides pure annotated ɵsetClassMetadataAsync', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (function () { + i0.ɵsetClassMetadataAsync(SomeClass, + function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; }, + function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, LocalDep], + template: '{#defer}{/defer}', + }] + }], null, null); }); + })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (function () { void 0 })(); + `, + }), + ); + + it( + 'elides JIT mode protected ɵsetClassMetadataAsync', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + (function () { + (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵsetClassMetadataAsync(SomeClass, + function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; }, + function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, LocalDep], + template: '{#defer}{/defer}', + }] + }], null, null); }); + })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + (function () { (typeof ngJitMode === "undefined" || ngJitMode) && void 0 })(); + `, + }), + ); + + it( + 'elides arrow-function-based ɵsetClassMetadataAsync', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (() => { + i0.ɵsetClassMetadataAsync(SomeClass, + () => [import("./cmp-a").then(m => m.CmpA)], + (CmpA) => { i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, LocalDep], + template: '{#defer}{/defer}', + }] + }], null, null); }); + })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (() => { void 0 })(); + `, + }), + ); + + it( + 'elides ɵsetClassDebugInfo', + testCase({ + input: ` + import { Component } from '@angular/core'; + class SomeClass {} + (() => { + (typeof ngDevMode === 'undefined' || ngDevMode) && + i0.ɵsetClassDebugInfo(SomeClass, { className: 'SomeClass' }); + })(); + `, + expected: ` + import { Component } from "@angular/core"; + class SomeClass {} + (() => { + (typeof ngDevMode === "undefined" || ngDevMode) && void 0; + })(); + `, + }), + ); +}); diff --git a/packages/angular/build/src/tools/oxc/oxc-transform.ts b/packages/angular/build/src/tools/oxc/oxc-transform.ts new file mode 100644 index 000000000000..c0389a9f75a4 --- /dev/null +++ b/packages/angular/build/src/tools/oxc/oxc-transform.ts @@ -0,0 +1,790 @@ +/** + * @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 { DecodedSourceMap } from '@ampproject/remapping'; +import { needsLinking } from '@angular/compiler-cli/linker'; +import type { BindingIdentifier, Class, Node } from '@oxc-project/types'; +import { MagicString } from 'magic-string'; +import { Visitor, parseSync } from 'oxc-parser'; +import { OxcLinker } from '../angular/linker/oxc-linker'; + +export interface OxcTransformOptions { + sourcemap?: boolean; + sideEffects?: boolean; + topLevelSafeMode?: boolean; + pureAnnotate?: boolean; + link?: boolean; + jit?: boolean; + advancedOptimizations?: boolean; +} + +/** + * 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([ + '__extends', + '__assign', + '__rest', + '__decorate', + '__param', + '__esDecorate', + '__runInitializers', + '__propKey', + '__setFunctionName', + '__metadata', + '__awaiter', + '__generator', + '__exportStar', + '__values', + '__read', + '__privateGet', + '__privateSet', + '__privateMethod', + '__addDisposableResource', + '__disposeResources', +]); + +/** + * Determines whether an identifier name matches one of the TypeScript helper function names. + * + * @param name The identifier name to check. + * @returns True if the name matches a TypeScript helper name; otherwise, false. + */ +function isTslibHelperName(name: string): boolean { + const nameParts = name.split('$'); + const originalName = nameParts[0]; + + if (nameParts.length > 2 || (nameParts.length === 2 && !/^\d+$/.test(nameParts[1]))) { + return false; + } + + return tslibHelpers.has(originalName); +} + +/** + * A set of Babel helper function names that are intended to cause side effects. + */ +const babelHelpers = new Set(['_defineProperty']); + +/** + * Determines whether an identifier name matches one of the Babel helper function names. + * + * @param name The identifier name to check. + * @returns True if the name matches a Babel helper name; otherwise, false. + */ +function isBabelHelperName(name: string): boolean { + return babelHelpers.has(name); +} + +/** + * A set of Angular static properties that should be wrapped in pure IIFE statements. + */ +const angularStaticsToWrap = new Set([ + 'ɵcmp', + 'ɵdir', + 'ɵfac', + 'ɵinj', + 'ɵmod', + 'ɵpipe', + 'ɵprov', + 'INJECTOR_KEY', +]); + +/** + * A set of Angular metadata decorator functions that can be elided. + */ +const angularMetadataFunctions = new Set([ + 'ɵsetClassMetadata', + 'ɵsetClassMetadataAsync', + 'ɵsetClassDebugInfo', +]); + +/** + * A map of static properties and their matcher predicate functions to check if they + * can be safely elided from class declarations. + */ +const angularStaticsToElide: Record boolean> = { + 'ctorParameters'(node) { + return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'; + }, + 'decorators'(node) { + return node.type === 'ArrayExpression'; + }, + 'propDecorators'(node) { + return node.type === 'ObjectExpression'; + }, +}; + +/** + * Determines whether an AST node is considered safe for pure evaluation (side-effect free). + * + * @param node The AST node to check. + * @returns True if the node is side-effect free; otherwise, false. + */ +function isPure(node: Node): boolean { + switch (node.type) { + case 'Identifier': + case 'Literal': + return true; + case 'BinaryExpression': + case 'LogicalExpression': + return isPure(node.left) && isPure(node.right); + case 'UnaryExpression': + return isPure(node.argument); + case 'MemberExpression': + return isPure(node.object) && (!node.computed || isPure(node.property)); + case 'ObjectExpression': + return node.properties.every((p) => p.type === 'Property' && isPure(p.value)); + case 'ArrayExpression': + return node.elements.every((e) => !e || isPure(e)); + case 'ParenthesizedExpression': + return isPure(node.expression); + default: + return false; + } +} + +/** + * Recursively unwraps any ParenthesizedExpression wrapper nodes to get the inner expression. + * + * @param node The potentially parenthesized AST node. + * @returns The inner non-parenthesized AST node. + */ +function unwrapParentheses(node: Node): Node { + while (node && node.type === 'ParenthesizedExpression') { + node = node.expression; + } + + return node; +} + +/** + * Determines whether a class static property assignment is safe to be wrapped in a pure IIFE. + * + * @param propertyName The name of the property. + * @param assignmentValue The AST node representing the value assigned. + * @param code The source code of the file. + * @returns True if the property can be wrapped safely; otherwise, false. + */ +function canWrapProperty(propertyName: string, assignmentValue: Node, code: string): boolean { + if (angularStaticsToWrap.has(propertyName)) { + return true; + } + + const prefix = code.substring(Math.max(0, assignmentValue.start - 100), assignmentValue.start); + if (/(\/\*[\s\S]*?(?:@__PURE__|#__PURE__|@pureOrBreakMyCode)[\s\S]*?\*\/)\s*$/.test(prefix)) { + return true; + } + + return isPure(assignmentValue); +} + +/** + * Analyzes static properties inside a class body to determine if the class has any + * side-effecting static property initializers. + * + * @param classNode The Class AST node. + * @param code The source code of the file. + * @returns True if the class static properties are pure and can be wrapped; otherwise, false. + */ +function analyzeClassStaticProperties(classNode: Node, code: string): boolean { + let shouldWrap = false; + const body = (classNode as Class).body.body; + for (const element of body) { + if (element.type === 'PropertyDefinition') { + if (!element.static) { + continue; + } + + const key = element.key; + const value = element.value; + if (key.type === 'Identifier' && (!value || canWrapProperty(key.name, value, code))) { + shouldWrap = true; + } else { + shouldWrap = false; + break; + } + } else if (element.type === 'StaticBlock') { + const blockBody = element.body; + if (blockBody.length === 0) { + continue; + } + if (blockBody.length > 1) { + shouldWrap = false; + break; + } + const expressionStatement = blockBody[0]; + if (expressionStatement && expressionStatement.type === 'ExpressionStatement') { + const assignment = expressionStatement.expression; + if ( + assignment && + assignment.type === 'AssignmentExpression' && + assignment.left.type === 'MemberExpression' + ) { + const left = assignment.left; + if (left.object.type === 'ThisExpression' && left.property.type === 'Identifier') { + if (canWrapProperty(left.property.name, assignment.right, code)) { + shouldWrap = true; + continue; + } + } + } + } + shouldWrap = false; + break; + } + } + + return shouldWrap; +} + +/** + * Executes a single-pass optimized transformation using oxc-parser and magic-string. + * Performs typescript enum wrapping, static class members elision/wrapping, angular metadata elision, + * and top-level pure function annotations. + * + * @param filename The absolute path of the file being transformed. + * @param code The string source content of the file. + * @param options Configuration options specifying which optimization steps to run. + * @returns The transformed code string and an optional source map. + */ +// eslint-disable-next-line max-lines-per-function +export function transform(filename: string, code: string, options: OxcTransformOptions) { + const { program } = parseSync(filename, code, { range: true }); + const source = new MagicString(code); + + const shouldLink = options.link && needsLinking(filename, code); + const linker = shouldLink ? new OxcLinker(filename, code, options.jit) : undefined; + + const advancedOptimizations = options.advancedOptimizations ?? true; + const sideEffectFree = options.sideEffects === false; + const topLevelSafeMode = options.topLevelSafeMode ?? false; + const wrapDecorators = sideEffectFree; + const pureAnnotate = options.pureAnnotate ?? true; + + /** + * Scans backwards from the specified start index to check if a pure comment (e.g. `/*@__PURE__*\/`) + * already precedes the node. + * + * @param start The index where the node starts. + * @returns True if a pure comment is already present; otherwise, false. + */ + function hasPureComment(start: number): boolean { + let i = start - 1; + while (i >= 0 && /\s/.test(code[i])) { + i--; + } + if (i < 1 || code[i] !== '/' || code[i - 1] !== '*') { + return false; + } + const commentEnd = i + 1; + const commentStart = code.lastIndexOf('/*', commentEnd); + if (commentStart === -1) { + return false; + } + const commentContent = code.substring(commentStart + 2, commentEnd - 2); + + return commentContent.includes('@__PURE__') || commentContent.includes('#__PURE__'); + } + + const editedRanges: { start: number; end: number }[] = []; + + /** + * Records a range in the source code that has been modified, preventing subsequent nested mutations. + * + * @param start The start index of the modified range. + * @param end The end index of the modified range. + */ + function markEdited(start: number, end: number) { + editedRanges.push({ start, end }); + } + + /** + * Checks if the specified range falls inside an already modified section of code. + * + * @param start The start index of the range. + * @param end The end index of the range. + * @returns True if the range is already edited; otherwise, false. + */ + function isAlreadyEdited(start: number, end: number): boolean { + return editedRanges.some((r) => start >= r.start && end <= r.end); + } + + // Track function nesting depth and closest function expression wrapper + let functionDepth = 0; + let classDepth = 0; + const functionStack: Node[] = []; + + /** + * Scans and rewrites TypeScript emitted enum declarations in the statement block. + * Wraps enum statements inside a pure IIFE assignable directly to the enum variable. + * + * @param body The array of statement AST nodes to process. + */ + function adjustTypeScriptEnumsInStatements(body: Node[]) { + for (let i = 0; i < body.length - 1; i++) { + const statement = body[i]; + let declStatement = statement; + if ( + statement.type === 'ExportNamedDeclaration' && + statement.declaration?.type === 'VariableDeclaration' + ) { + declStatement = statement.declaration; + } + + if ( + declStatement.type !== 'VariableDeclaration' || + declStatement.kind !== 'var' || + declStatement.declarations.length !== 1 + ) { + continue; + } + const decl = declStatement.declarations[0]; + if (decl.init || decl.id.type !== 'Identifier') { + continue; + } + + const nextStatement = body[i + 1]; + if (nextStatement.type !== 'ExpressionStatement') { + continue; + } + + const nextExpr = unwrapParentheses(nextStatement.expression); + if (nextExpr.type !== 'CallExpression' || nextExpr.arguments.length !== 1) { + continue; + } + + const arg = unwrapParentheses(nextExpr.arguments[0]); + if (arg.type !== 'LogicalExpression' || arg.operator !== '||') { + continue; + } + + const argLeft = unwrapParentheses(arg.left); + if (argLeft.type !== 'Identifier' || argLeft.name !== decl.id.name) { + continue; + } + + const rightCallArgument = unwrapParentheses(arg.right); + if (rightCallArgument.type !== 'AssignmentExpression') { + continue; + } + + const callee = unwrapParentheses(nextExpr.callee); + if ( + callee.type !== 'FunctionExpression' || + callee.params.length !== 1 || + !callee.body || + callee.body.type !== 'BlockStatement' + ) { + continue; + } + + const param = callee.params[0]; + if (param.type !== 'Identifier') { + continue; + } + const paramName = (param as BindingIdentifier).name; + + // Check if all statements in body are pure assignments + let hasElements = false; + let allPure = true; + for (const enumStatement of callee.body.body) { + if (enumStatement.type !== 'ExpressionStatement') { + allPure = false; + break; + } + + const enumValueAssignment = unwrapParentheses(enumStatement.expression); + if ( + enumValueAssignment.type !== 'AssignmentExpression' || + !isPure(enumValueAssignment.right) + ) { + allPure = false; + break; + } + + hasElements = true; + } + + if (!allPure || !hasElements) { + continue; + } + + // 1. Remove leading/trailing characters/parentheses of the expression statement + source.remove(nextStatement.start, nextExpr.start); + source.remove(nextExpr.end, nextStatement.end); + markEdited(nextStatement.start, nextStatement.end); + + // 2. Add return statement inside IIFE body + source.appendRight(callee.body.end - 1, `; return ${paramName};`); + + // 3. Remove `Name = ` assignment in arguments if it's a simple identifier + if (rightCallArgument.left.type === 'Identifier') { + let replacement = code.substring( + rightCallArgument.right.start, + rightCallArgument.right.end, + ); + if (unwrapParentheses(rightCallArgument.right).type === 'AssignmentExpression') { + replacement = `(${replacement})`; + } + source.overwrite(arg.right.start, arg.right.end, replacement); + markEdited(arg.right.start, arg.right.end); + } + + // 4. Move IIFE to the var initializer + source.move(nextExpr.start, nextExpr.end, decl.id.end); + source.appendLeft(decl.id.end, ' = /*#__PURE__*/ '); + } + } + + /** + * Scans and rewrites static class member initializers in the statement block. + * Groups externalized class static assignments into pure wrappers or elides them when safe. + * + * @param body The array of statement AST nodes to process. + */ + function adjustStaticMembersInStatements(body: Node[]) { + for (let i = 0; i < body.length; i++) { + const statement = body[i]; + let classNode: Node | null = null; + let isExportDefault = false; + let isExportNamed = false; + let isVariableClass = false; + let classIdName = ''; + if (statement.type === 'ClassDeclaration') { + classNode = statement; + classIdName = classNode.id?.name || ''; + } else if ( + statement.type === 'ExportNamedDeclaration' && + statement.declaration?.type === 'ClassDeclaration' + ) { + classNode = statement.declaration; + classIdName = classNode.id?.name || ''; + isExportNamed = true; + } else if ( + statement.type === 'ExportDefaultDeclaration' && + statement.declaration?.type === 'ClassDeclaration' + ) { + classNode = statement.declaration; + classIdName = classNode.id?.name || ''; + isExportDefault = true; + } else if (statement.type === 'VariableDeclaration' && statement.declarations.length === 1) { + const decl = statement.declarations[0]; + if (decl.init && decl.init.type === 'ClassExpression' && decl.id.type === 'Identifier') { + classNode = decl.init; + classIdName = decl.id.name; + isVariableClass = true; + } + } + + if (!classNode || !classIdName) { + continue; + } + + const wrapStatementPaths: { statement: Node; type: 'wrap' | 'decorate' | 'elide' }[] = []; + let hasPotentialSideEffects = false; + + for (let j = i + 1; j < body.length; j++) { + const nextStatement = body[j]; + if (nextStatement.type !== 'ExpressionStatement') { + break; + } + + const nextExpression = nextStatement.expression; + + // Case 1: __decorate(...) + if (nextExpression.type === 'CallExpression') { + if ( + nextExpression.callee.type !== 'Identifier' || + nextExpression.callee.name !== '__decorate' + ) { + break; + } + + if (wrapDecorators) { + wrapStatementPaths.push({ statement: nextStatement, type: 'decorate' }); + } else { + hasPotentialSideEffects = true; + } + continue; + } + + // Case 2: AssignmentExpression + if (nextExpression.type !== 'AssignmentExpression') { + break; + } + + const left = nextExpression.left; + + if (left.type === 'Identifier') { + if ( + left.name !== classIdName || + nextExpression.right.type !== 'CallExpression' || + nextExpression.right.callee.type !== 'Identifier' || + nextExpression.right.callee.name !== '__decorate' + ) { + break; + } + + if (wrapDecorators) { + wrapStatementPaths.push({ statement: nextStatement, type: 'decorate' }); + } else { + hasPotentialSideEffects = true; + } + continue; + } + + if ( + left.type !== 'MemberExpression' || + left.object.type !== 'Identifier' || + left.object.name !== classIdName || + left.property.type !== 'Identifier' + ) { + break; + } + + const propertyName = left.property.name; + const assignmentValue = nextExpression.right; + + if (angularStaticsToElide[propertyName]?.(assignmentValue)) { + wrapStatementPaths.push({ statement: nextStatement, type: 'elide' }); + } else if (canWrapProperty(propertyName, assignmentValue, code)) { + wrapStatementPaths.push({ statement: nextStatement, type: 'wrap' }); + } else { + hasPotentialSideEffects = true; + } + } + + // Check class body static properties + const shouldWrapClassStaticProperties = analyzeClassStaticProperties(classNode, code); + + // Perform elisions immediately + for (const item of wrapStatementPaths) { + if (item.type === 'elide') { + source.remove(item.statement.start, item.statement.end); + markEdited(item.statement.start, item.statement.end); + } + } + + const activeWrapPaths = wrapStatementPaths.filter( + (p) => p.type === 'wrap' || p.type === 'decorate', + ); + + if ( + !hasPotentialSideEffects && + (activeWrapPaths.length > 0 || shouldWrapClassStaticProperties) + ) { + const lastStatement = + activeWrapPaths.length > 0 + ? activeWrapPaths[activeWrapPaths.length - 1].statement + : classNode; + + if (isExportDefault) { + // 1. Remove `export default ` + source.overwrite(statement.start, classNode.start, ''); + // 2. Wrap in IIFE + source.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); + source.appendLeft( + lastStatement.end, + `\nreturn ${classIdName};\n})();\nexport { ${classIdName} as default };`, + ); + } else if (isExportNamed) { + // 1. Export is kept, turn `class` into `let ClassName = IIFE` + source.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); + source.appendLeft(lastStatement.end, `\nreturn ${classIdName};\n})();`); + } else if (isVariableClass) { + // Wrap class inside init: `/*#__PURE__*/ (() => { let ClassName = class ClassName {}; return ClassName; })()` + source.appendRight(classNode.start, `/*#__PURE__*/ (() => {\nlet ${classIdName} = `); + const terminator = activeWrapPaths.length === 0 ? ';' : ''; + const iifeClosing = activeWrapPaths.length === 0 ? '})()' : '})();'; + source.appendLeft( + lastStatement.end, + `${terminator}\nreturn ${classIdName};\n${iifeClosing}`, + ); + } else { + // Standard ClassDeclaration + source.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); + source.appendLeft(lastStatement.end, `\nreturn ${classIdName};\n})();`); + } + + markEdited(statement.start, lastStatement.end); + + // Fast-forward outer loop index to skip the statements we wrapped + i += wrapStatementPaths.length; + } else if (isExportDefault && !hasPotentialSideEffects) { + // Splitting default export even when not wrapped + source.overwrite(statement.start, classNode.start, ''); + source.appendLeft(classNode.end, `\nexport { ${classIdName} as default };`); + markEdited(statement.start, classNode.end); + } + } + } + + const visitor = new Visitor({ + ClassDeclaration(node) { + classDepth++; + }, + 'ClassDeclaration:exit'() { + classDepth--; + }, + ClassExpression(node) { + classDepth++; + }, + 'ClassExpression:exit'() { + classDepth--; + }, + FunctionDeclaration(node) { + functionDepth++; + functionStack.push(node); + }, + 'FunctionDeclaration:exit'() { + functionDepth--; + functionStack.pop(); + }, + FunctionExpression(node) { + functionDepth++; + functionStack.push(node); + }, + 'FunctionExpression:exit'() { + functionDepth--; + functionStack.pop(); + }, + ArrowFunctionExpression(node) { + functionDepth++; + functionStack.push(node); + }, + 'ArrowFunctionExpression:exit'() { + functionDepth--; + functionStack.pop(); + }, + 'Program:exit'(node) { + if (advancedOptimizations) { + adjustTypeScriptEnumsInStatements(node.body); + adjustStaticMembersInStatements(node.body); + } + }, + 'BlockStatement:exit'(node) { + if (advancedOptimizations) { + adjustTypeScriptEnumsInStatements(node.body); + adjustStaticMembersInStatements(node.body); + } + }, + CallExpression(node) { + if (isAlreadyEdited(node.start, node.end)) { + return; + } + + if (linker) { + const linkedCode = linker.linkCallExpression(node); + if (linkedCode !== undefined) { + source.overwrite(node.start, node.end, linkedCode); + markEdited(node.start, node.end); + + return; + } + } + + if (!advancedOptimizations) { + return; + } + + // 1. Elide Angular Metadata check + let calleeName: string | undefined; + if (node.callee.type === 'Identifier') { + calleeName = node.callee.name; + } else if ( + node.callee.type === 'MemberExpression' && + node.callee.property.type === 'Identifier' + ) { + calleeName = node.callee.property.name; + } + + if (calleeName && angularMetadataFunctions.has(calleeName)) { + const parentFunc = functionStack[functionStack.length - 1]; + if ( + parentFunc && + (parentFunc.type === 'FunctionExpression' || + parentFunc.type === 'ArrowFunctionExpression') + ) { + source.overwrite(node.start, node.end, 'void 0'); + markEdited(node.start, node.end); + + return; + } + } + + // 2. Mark Top-Level Pure Functions check + if (!pureAnnotate || functionDepth > 0 || classDepth > 0 || topLevelSafeMode) { + return; + } + + const callee = unwrapParentheses(node.callee); + if ( + (callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') && + node.arguments.length !== 0 + ) { + return; + } + + if ( + callee.type === 'Identifier' && + (isTslibHelperName(callee.name) || isBabelHelperName(callee.name)) + ) { + return; + } + + if (!hasPureComment(node.start)) { + source.appendLeft(node.start, '/*#__PURE__*/ '); + } + }, + NewExpression(node) { + if ( + !advancedOptimizations || + !pureAnnotate || + functionDepth > 0 || + classDepth > 0 || + isAlreadyEdited(node.start, node.end) + ) { + return; + } + + if (!topLevelSafeMode) { + if (!hasPureComment(node.start)) { + source.appendLeft(node.start, '/*#__PURE__*/ '); + } + + return; + } + + const callee = node.callee; + if (callee.type === 'Identifier' && sideEffectFreeConstructors.has(callee.name)) { + if (!hasPureComment(node.start)) { + source.appendLeft(node.start, '/*#__PURE__*/ '); + } + } + }, + }); + + visitor.visit(program); + + let map: DecodedSourceMap | undefined; + if (options.sourcemap) { + const rawMap = source.generateDecodedMap({ hires: true, source: filename }); + map = { ...rawMap, version: 3 }; + } + + return { + code: source.toString(), + map, + }; +} diff --git a/packages/angular/build/src/tools/oxc/oxc-transform_spec.ts b/packages/angular/build/src/tools/oxc/oxc-transform_spec.ts new file mode 100644 index 000000000000..d1b02f6a1bdc --- /dev/null +++ b/packages/angular/build/src/tools/oxc/oxc-transform_spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { transform } from './oxc-transform'; + +describe('oxc-transform', () => { + describe('sourcemaps', () => { + it('should generate a decoded sourcemap when sourcemap option is enabled', () => { + const input = 'var result = new SomeClass();'; + const result = transform('test.js', input, { sourcemap: true }); + + expect(result.map).toBeDefined(); + expect(result.map?.version).toBe(3); + expect(result.map?.sources).toContain('test.js'); + expect(result.map?.mappings.length).toBeGreaterThan(0); + }); + }); + + describe('linking and unified passes', () => { + const componentInput = ` + import * as i0 from "@angular/core"; + export class MyComponent {} + MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyComponent, + isStandalone: true, + selector: "my-cmp", + template: "Hello" + }); + `; + + it('should link partial component declarations when link option is enabled', () => { + const result = transform('test.js', componentInput, { link: true }); + expect(result.code).toContain('i0.ɵɵdefineComponent'); + expect(result.code).not.toContain('i0.ɵɵngDeclareComponent'); + }); + + it('should not link partial component declarations when link option is disabled', () => { + const result = transform('test.js', componentInput, { link: false }); + expect(result.code).not.toContain('i0.ɵɵdefineComponent'); + expect(result.code).toContain('i0.ɵɵngDeclareComponent'); + }); + + it('should perform linking and advanced optimizations simultaneously in a single pass', () => { + const input = ` + import * as i0 from "@angular/core"; + export class MyComponent { + static create() { + return new MyComponent(); + } + } + MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyComponent, + isStandalone: true, + selector: "my-cmp", + template: "Hello" + }); + `; + + const result = transform('test.js', input, { + link: true, + advancedOptimizations: true, + topLevelSafeMode: true, + }); + expect(result.code).toContain('i0.ɵɵdefineComponent'); + expect(result.code).not.toContain('i0.ɵɵngDeclareComponent'); + expect(result.code).toContain('let MyComponent = /*#__PURE__*/ (() => {'); + }); + }); +}); diff --git a/packages/angular/build/src/tools/oxc/pure-toplevel-functions_oxc_spec.ts b/packages/angular/build/src/tools/oxc/pure-toplevel-functions_oxc_spec.ts new file mode 100644 index 000000000000..0b91cc98aa45 --- /dev/null +++ b/packages/angular/build/src/tools/oxc/pure-toplevel-functions_oxc_spec.ts @@ -0,0 +1,202 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { transform } from './oxc-transform'; + +function cleanCode(code: string): string { + return code + .replace(/\s+/g, '') + .replace(/"/g, "'") + .replace(/;}/g, '}') + .replace(/,([}\])])/g, '$1'); +} + +function testCase({ + input, + expected, + options, +}: { + input: string; + expected: string; + options?: { topLevelSafeMode: boolean }; +}): jasmine.ImplementationCallback { + return async () => { + const result = transform('test.js', input, { + sourcemap: false, + topLevelSafeMode: options?.topLevelSafeMode, + }); + if (!result?.code) { + fail('Expected oxc-transform to return a transform result.'); + } else { + const actualClean = cleanCode(result.code); + const expectedClean = cleanCode(expected); + expect(actualClean).toEqual(expectedClean); + } + }; +} + +function testCaseNoChange(input: string): jasmine.ImplementationCallback { + return testCase({ input, expected: input }); +} + +describe('pure-toplevel-functions oxc-transform implementation', () => { + it( + 'annotates top-level new expressions', + testCase({ + input: 'var result = new SomeClass();', + expected: 'var result = /*#__PURE__*/ new SomeClass();', + }), + ); + + it( + 'annotates top-level function calls', + testCase({ + input: 'var result = someCall();', + expected: 'var result = /*#__PURE__*/ someCall();', + }), + ); + + it( + 'annotates top-level IIFE assignments with no arguments', + testCase({ + input: 'var SomeClass = (function () { function SomeClass() { } return SomeClass; })();', + expected: + 'var SomeClass = /*#__PURE__*/(function () { function SomeClass() { } return SomeClass; })();', + }), + ); + + it( + 'annotates top-level arrow-function-based IIFE assignments with no arguments', + testCase({ + input: 'var SomeClass = (() => { function SomeClass() { } return SomeClass; })();', + expected: + 'var SomeClass = /*#__PURE__*/(() => { function SomeClass() { } return SomeClass; })();', + }), + ); + + it( + 'does not annotate top-level IIFE assignments with arguments', + testCaseNoChange( + 'var SomeClass = (function () { function SomeClass() { } return SomeClass; })(abc);', + ), + ); + + it( + 'does not annotate top-level arrow-function-based IIFE assignments with arguments', + testCaseNoChange( + 'var SomeClass = (() => { function SomeClass() { } return SomeClass; })(abc);', + ), + ); + + it( + 'does not annotate call expressions inside function declarations', + testCaseNoChange('function funcDecl() { const result = someFunction(); }'), + ); + + it( + 'does not annotate call expressions inside function expressions', + testCaseNoChange('const foo = function funcDecl() { const result = someFunction(); }'), + ); + + it( + 'does not annotate call expressions inside arrow functions', + testCaseNoChange('const foo = () => { const result = someFunction(); }'), + ); + + it( + 'does not annotate new expressions inside function declarations', + testCaseNoChange('function funcDecl() { const result = new SomeClass(); }'), + ); + + it( + 'does not annotate new expressions inside function expressions', + testCaseNoChange('const foo = function funcDecl() { const result = new SomeClass(); }'), + ); + + it( + 'does not annotate new expressions inside arrow functions', + testCaseNoChange('const foo = () => { const result = new SomeClass(); }'), + ); + + it( + 'does not annotate TypeScript helper functions (tslib)', + testCaseNoChange(` + class LanguageState {} + __decorate([ + __metadata("design:type", Function), + __metadata("design:paramtypes", [Object]), + __metadata("design:returntype", void 0) + ], LanguageState.prototype, "checkLanguage", null); + `), + ); + + it( + 'does not annotate _defineProperty function', + testCaseNoChange(` + class LanguageState {} + _defineProperty( + LanguageState, + 'property', + 'value' + ); + `), + ); + + it( + 'does not annotate object literal methods', + testCaseNoChange(` + const literal = { + method() { + var newClazz = new Clazz(); + } + }; + `), + ); + + it( + 'annotates helper functions with non-numeric suffixes', + testCase({ + input: 'var result = __decorate$foo();', + expected: 'var result = /*#__PURE__*/ __decorate$foo();', + }), + ); + + it( + 'does not annotate helper functions with numeric suffixes', + testCaseNoChange('var result = __decorate$1();'), + ); + + 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/oxc/types.d.ts b/packages/angular/build/src/tools/oxc/types.d.ts new file mode 100644 index 000000000000..aa1f40580491 --- /dev/null +++ b/packages/angular/build/src/tools/oxc/types.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +declare module 'istanbul-lib-instrument' { + export interface Instrumenter { + instrumentSync(code: string, filename: string, inputSourceMap?: object): string; + lastSourceMap(): object | undefined; + } + + export function createInstrumenter(options?: { + produceSourceMap?: boolean; + esModules?: boolean; + coverageVariable?: string; + }): Instrumenter; +} diff --git a/packages/angular/build/src/tools/sass/rebasing-importer.ts b/packages/angular/build/src/tools/sass/rebasing-importer.ts index 15c94a25aeef..5d2a4ddc2267 100644 --- a/packages/angular/build/src/tools/sass/rebasing-importer.ts +++ b/packages/angular/build/src/tools/sass/rebasing-importer.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { RawSourceMap } from '@ampproject/remapping'; -import MagicString from 'magic-string'; +import type { DecodedSourceMap } from '@ampproject/remapping'; +import { MagicString } from 'magic-string'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { basename, dirname, extname, join, relative } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -44,7 +44,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> { */ constructor( private entryDirectory: string, - private rebaseSourceMaps?: Map, + private rebaseSourceMaps?: Map, ) {} abstract canonicalize(url: string, options: { fromImport: boolean }): URL | null; @@ -95,12 +95,15 @@ abstract class UrlRebasingImporter implements Importer<'sync'> { contents = updatedContents.toString(); if (this.rebaseSourceMaps) { // Generate an intermediate source map for the rebasing changes - const map = updatedContents.generateMap({ + const map = updatedContents.generateDecodedMap({ hires: 'boundary', includeContent: true, source: canonicalUrl.href, }); - this.rebaseSourceMaps.set(canonicalUrl.href, map as RawSourceMap); + this.rebaseSourceMaps.set(canonicalUrl.href, { + ...map, + version: 3, + } satisfies DecodedSourceMap); } } @@ -134,7 +137,7 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter { constructor( entryDirectory: string, private directoryCache = new Map(), - rebaseSourceMaps?: Map, + rebaseSourceMaps?: Map, ) { super(entryDirectory, rebaseSourceMaps); } @@ -322,7 +325,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter { constructor( entryDirectory: string, directoryCache: Map, - rebaseSourceMaps: Map | undefined, + rebaseSourceMaps: Map | undefined, private finder: (specifier: string, options: CanonicalizeContext) => URL | null, ) { super(entryDirectory, directoryCache, rebaseSourceMaps); @@ -349,7 +352,7 @@ export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter { constructor( entryDirectory: string, directoryCache: Map, - rebaseSourceMaps: Map | undefined, + rebaseSourceMaps: Map | undefined, private loadPaths: Iterable, ) { super(entryDirectory, directoryCache, rebaseSourceMaps); diff --git a/packages/angular/build/src/tools/sass/worker.ts b/packages/angular/build/src/tools/sass/worker.ts index 1a2e1184892f..e4167a3d1c69 100644 --- a/packages/angular/build/src/tools/sass/worker.ts +++ b/packages/angular/build/src/tools/sass/worker.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import mergeSourceMaps, { RawSourceMap } from '@ampproject/remapping'; +import mergeSourceMaps, { type DecodedSourceMap, type RawSourceMap } from '@ampproject/remapping'; import { dirname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { MessagePort, receiveMessageOnPort } from 'node:worker_threads'; @@ -89,7 +89,7 @@ export default async function renderSassStylesheet( let warnings: SerializableWarningMessage[] | undefined; try { const directoryCache = new Map(); - const rebaseSourceMaps = options.sourceMap ? new Map() : undefined; + const rebaseSourceMaps = options.sourceMap ? new Map() : undefined; if (importerChannel) { // When a custom importer function is present, the importer request must be proxied // back to the main thread where it can be executed. 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 e0074625afe0..02f54756eaff 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -7,12 +7,14 @@ */ import { lookup as lookupMimeType } from 'mrmime'; -import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import type { ServerResponse } from 'node:http'; import { extname } from 'node:path'; -import type { Connect, ViteDevServer } from 'vite'; +import type { Connect, ViteDevServer } from 'vite' with { + 'resolution-mode': 'import', +}; import { ResultFile } from '../../../builders/application/results'; +import { calculateHash } from '../../../utils/hash'; import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils'; export interface ComponentStyleRecord { @@ -48,7 +50,7 @@ export function createAngularAssetsMiddleware( // This is a workaround to serve extensionless, CSS, JS and TS files without Vite transformations. if (!extension || JS_TS_REGEXP.test(extension) || CSS_PREPROCESSOR_REGEXP.test(extension)) { const contents = readFileSync(asset.source); - const etag = `W/${createHash('sha256').update(contents).digest('hex')}`; + const etag = `W/${calculateHash(contents)}`; if (checkAndHandleEtag(req, res, etag)) { return; } @@ -236,7 +238,7 @@ export function createBuildAssetsMiddleware( const contents = outputFile.origin === 'memory' ? outputFile.contents : readHandler(outputFile.inputPath); - const etag = `W/${createHash('sha256').update(contents).digest('hex')}`; + const etag = `W/${calculateHash(contents)}`; if (checkAndHandleEtag(req, res, etag)) { return; } diff --git a/packages/angular/build/src/tools/vite/middlewares/base-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/base-middleware.ts index 00198e03061a..a9df78fd2981 100644 --- a/packages/angular/build/src/tools/vite/middlewares/base-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/base-middleware.ts @@ -7,7 +7,9 @@ */ import type { IncomingMessage, ServerResponse } from 'node:http'; -import type { Connect } from 'vite'; +import type { Connect } from 'vite' with { + 'resolution-mode': 'import', +}; import { addLeadingSlash } from '../../../utils/url'; /** diff --git a/packages/angular/build/src/tools/vite/middlewares/chrome-devtools-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/chrome-devtools-middleware.ts index 43c5bed2edf9..fdeff2eec705 100644 --- a/packages/angular/build/src/tools/vite/middlewares/chrome-devtools-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/chrome-devtools-middleware.ts @@ -10,7 +10,9 @@ import assert from 'node:assert'; import { randomUUID } from 'node:crypto'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import type { Connect } from 'vite'; +import type { Connect } from 'vite' with { + 'resolution-mode': 'import', +}; type DevToolsJson = { workspace: { diff --git a/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts index 0e02616cb384..29360d78af55 100644 --- a/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { Connect, ViteDevServer } from 'vite'; +import type { Connect, ViteDevServer } from 'vite' with { + 'resolution-mode': 'import', +}; import { pathnameWithoutBasePath } from '../utils'; const ANGULAR_COMPONENT_PREFIX = '/@ng/component'; diff --git a/packages/angular/build/src/tools/vite/middlewares/headers-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/headers-middleware.ts index 46d4a88f0543..c7f9408cec4a 100644 --- a/packages/angular/build/src/tools/vite/middlewares/headers-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/headers-middleware.ts @@ -7,7 +7,9 @@ */ import type { ServerResponse } from 'node:http'; -import type { Connect, ViteDevServer } from 'vite'; +import type { Connect, ViteDevServer } from 'vite' with { + 'resolution-mode': 'import', +}; /** * Creates a middleware for adding custom headers. diff --git a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts index 8561354812b3..a4d49f4a9eeb 100644 --- a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts @@ -7,7 +7,9 @@ */ import type { IncomingMessage, ServerResponse } from 'node:http'; -import type { Connect } from 'vite'; +import type { Connect } from 'vite' with { + 'resolution-mode': 'import', +}; export function patchHostValidationMiddleware(middlewares: Connect.Server): void { const entry = middlewares.stack.find( diff --git a/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.ts index cd52b8a7904f..dbef050ac4c0 100644 --- a/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.ts @@ -7,7 +7,9 @@ */ import type { ServerResponse } from 'node:http'; -import type { Connect } from 'vite'; +import type { Connect } from 'vite' with { + 'resolution-mode': 'import', +}; import { lookupMimeTypeFromRequest } from '../utils'; const ALLOWED_FALLBACK_METHODS = Object.freeze(['GET', 'HEAD']); diff --git a/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.ts index 7959ccb7ec03..7767d970ee42 100644 --- a/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.ts @@ -7,7 +7,9 @@ */ import { extname } from 'node:path'; -import type { Connect, ViteDevServer } from 'vite'; +import type { Connect, ViteDevServer } from 'vite' with { + 'resolution-mode': 'import', +}; import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils'; export function createAngularIndexHtmlMiddleware( 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 a26fa8e5e257..78458624c4db 100644 --- a/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts @@ -11,7 +11,9 @@ import type { ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp, } from '@angular/ssr'; import type { ServerResponse } from 'node:http'; -import type { Connect, ViteDevServer } from 'vite'; +import type { Connect, ViteDevServer } from 'vite' with { + 'resolution-mode': 'import', +}; import { isSsrNodeRequestHandler, isSsrRequestHandler, 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 be00e3437f27..c6dc243385a8 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 @@ -10,7 +10,10 @@ import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { Plugin } from 'vite'; +import type * as Vite from 'vite' with { + 'resolution-mode': 'import', +}; +import { removeSourceMappingURL } from '../../../utils/source-map'; import { AngularMemoryOutputFiles } from '../utils'; interface AngularMemoryPluginOptions { @@ -27,9 +30,9 @@ const FILE_PROTOCOL = 'file:'; export async function createAngularMemoryPlugin( options: AngularMemoryPluginOptions, -): Promise { +): Promise { const { virtualProjectRoot, outputFiles, external } = options; - const { normalizePath } = await import('vite'); + const { normalizePath } = (await import('vite' as string)) as typeof Vite; return { name: 'vite:angular-memory', @@ -102,7 +105,7 @@ export async function createAngularMemoryPlugin( return { // Remove source map URL comments from the code if a sourcemap is present. // Vite will inline and add an additional sourcemap URL for the sourcemap. - code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code, + code: mapContents ? removeSourceMappingURL(code) : code, map: mapContents && Buffer.from(mapContents).toString('utf-8'), }; }, diff --git a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts index 5e543734b863..74cd03179a7e 100644 --- a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { Plugin } from 'vite'; +import type { Plugin } from 'vite' with { + 'resolution-mode': 'import', +}; // NOTE: the implementation for this Vite plugin is roughly based on: // https://github.com/MilanKovacic/vite-plugin-externalize-dependencies @@ -27,11 +29,7 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin { return; } - const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?'); - const prefixedExternalRegex = new RegExp( - `${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`, - 'g', - ); + const transformFn = createTransformer(resolvedConfig.base, externals); // @ts-expect-error: Property 'push' does not exist on type 'readonly Plugin[]' // Reasoning: @@ -40,15 +38,34 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin { // AFTER the import-analysis. resolvedConfig.plugins.push({ name: 'angular-plugin-remove-id-prefix-transform', - transform: (code: string) => { - // don't do anything when code does not contain the Vite prefix - if (!code.includes(VITE_ID_PREFIX)) { - return code; - } - - return code.replace(prefixedExternalRegex, (_, externalName) => externalName); - }, + transform: transformFn, }); }, }; } + +/** + * Creates a transform function that removes the Vite ID prefix from externals. + * @param base The base path of the application. + * @param externals The external package names. + * @returns A function that transforms code by removing the Vite ID prefix. + */ +export function createTransformer(base: string, externals: string[]): (code: string) => string { + // The path suffix is bounded so that a match can never extend past the end of an + // import specifier string literal. With a greedy `.+`, minified (single-line) code + // would let the first match consume the remainder of the line, leaving all later + // `/@id/` occurrences on that line unstripped. + const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/[^\'"`\\s]+)?'); + + const prefixedExternalRegex = new RegExp( + `${base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`, + 'g', + ); + + return (code: string) => { + return code.includes(VITE_ID_PREFIX) + ? code.replace(prefixedExternalRegex, (_, externalName) => externalName) + : // don't do anything when code does not contain the Vite prefix + code; + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin_spec.ts b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin_spec.ts new file mode 100644 index 000000000000..37d1bdf61fe6 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin_spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { createTransformer } from './id-prefix-plugin'; + +describe('createTransformer', () => { + it('should strip the prefix from every occurrence on a single (minified) line', () => { + const transform = createTransformer('/', [ + '@angular/common', + '@angular/common/http', + '@angular/core', + '@angular/router', + ]); + + const minified = + 'import{a}from"/@id/@angular/common/http";' + + 'import{b}from"/@id/@angular/router";' + + 'import{c}from"/@id/@angular/core";'; + + expect(transform(minified)).toBe( + 'import{a}from"@angular/common/http";' + + 'import{b}from"@angular/router";' + + 'import{c}from"@angular/core";', + ); + }); + + it('should strip the prefix from an external with a deep import path', () => { + const transform = createTransformer('/', ['@angular/common']); + + expect(transform('import{h}from"/@id/@angular/common/http";')).toBe( + 'import{h}from"@angular/common/http";', + ); + }); + + it('should strip the prefix when a non-root base is configured', () => { + const transform = createTransformer('/app/', ['@angular/router']); + + expect(transform('import{r}from"/app/@id/@angular/router";')).toBe( + 'import{r}from"@angular/router";', + ); + }); + + it('should strip the prefix from multi-line (unminified) code', () => { + const transform = createTransformer('/', ['@angular/common', '@angular/router']); + + const code = + 'import { CommonModule } from "/@id/@angular/common";\n' + + 'import { Router } from "/@id/@angular/router";\n'; + + expect(transform(code)).toBe( + 'import { CommonModule } from "@angular/common";\n' + + 'import { Router } from "@angular/router";\n', + ); + }); + + it('should not modify imports that are not configured externals', () => { + const transform = createTransformer('/', ['@angular/router']); + + const code = 'import{x}from"/@id/some-other-package";'; + expect(transform(code)).toBe(code); + }); +}); 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 5d20d5c705ac..78e16304f01d 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 @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { Connect, Plugin } from 'vite'; +import type { Connect, Plugin } from 'vite' with { + 'resolution-mode': 'import', +}; import { ComponentStyleRecord, angularHtmlFallbackMiddleware, 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 index a32c87a604de..ceb1f9630b7f 100644 --- a/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts @@ -7,7 +7,9 @@ */ import { getCACertificates, setDefaultCACertificates } from 'node:tls'; -import type { Plugin } from 'vite'; +import type { Plugin } from 'vite' with { + 'resolution-mode': 'import', +}; export function createAngularServerSideSSLPlugin(): Plugin { return { 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 90d183acde02..b857cc3cde74 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 @@ -7,10 +7,12 @@ */ import remapping, { SourceMapInput } from '@ampproject/remapping'; -import type { Plugin } from 'vite'; +import type * as Vite from 'vite' with { + 'resolution-mode': 'import', +}; -export async function createAngularSsrTransformPlugin(workspaceRoot: string): Promise { - const { normalizePath } = await import('vite'); +export async function createAngularSsrTransformPlugin(workspaceRoot: string): Promise { + const { normalizePath } = (await import('vite' as string)) as typeof Vite; return { name: 'vite:angular-ssr-transform', diff --git a/packages/angular/build/src/tools/vite/utils.ts b/packages/angular/build/src/tools/vite/utils.ts index 7250fd93ceb7..dc7094b8ffe4 100644 --- a/packages/angular/build/src/tools/vite/utils.ts +++ b/packages/angular/build/src/tools/vite/utils.ts @@ -9,10 +9,11 @@ import { lookup as lookupMimeType } from 'mrmime'; import { builtinModules, isBuiltin } from 'node:module'; import { extname } from 'node:path'; -import type { DepOptimizationConfig } from 'vite'; +import type { DepOptimizationConfig } from 'vite' with { + 'resolution-mode': 'import', +}; import type { ExternalResultMetadata } from '../esbuild/bundler-execution-result'; import { JavaScriptTransformer } from '../esbuild/javascript-transformer'; -import { getFeatureSupport } from '../esbuild/utils'; export type AngularMemoryOutputFiles = Map< string, @@ -41,75 +42,63 @@ export function lookupMimeTypeFromRequest(url: string): string | undefined { return extension && lookupMimeType(extension); } -type ViteEsBuildPlugin = NonNullable< - NonNullable['plugins'] ->[0]; - -export type EsbuildLoaderOption = Exclude< - DepOptimizationConfig['esbuildOptions'], +export type RolldownLoaderOption = Exclude< + DepOptimizationConfig['rolldownOptions'], undefined ->['loader']; +>['moduleTypes']; export function getDepOptimizationConfig({ + target, disabled, exclude, include, - target, - zoneless, prebundleTransformer, - ssr, loader, thirdPartySourcemaps, define = {}, }: { + target: string[]; disabled: boolean; exclude: string[]; include: string[]; - target: string[]; prebundleTransformer: JavaScriptTransformer; - ssr: boolean; - zoneless: boolean; - loader?: EsbuildLoaderOption; + loader?: RolldownLoaderOption; thirdPartySourcemaps: boolean; define: Record | undefined; }): DepOptimizationConfig { - const plugins: ViteEsBuildPlugin[] = [ - { - name: `angular-vite-optimize-deps${ssr ? '-ssr' : ''}${ - thirdPartySourcemaps ? '-vendor-sourcemap' : '' - }`, - setup(build) { - build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => { - return { - contents: await prebundleTransformer.transformFile(args.path), - loader: 'js', - }; - }); - }, - }, - ]; - - return { + const config: DepOptimizationConfig = { // Exclude any explicitly defined dependencies (currently build defined externals) exclude, // NB: to disable the deps optimizer, set optimizeDeps.noDiscovery to true and optimizeDeps.include as undefined. // Include all implict dependencies from the external packages internal option include: disabled ? undefined : include, noDiscovery: disabled, - // Add an esbuild plugin to run the Angular linker on dependencies - esbuildOptions: { - // Set esbuild supported targets. - target, - supported: getFeatureSupport(zoneless), - plugins, - loader, - define: { - ...define, - 'ngServerMode': `${ssr}`, + rolldownOptions: { + transform: { + target, + define, }, - resolveExtensions: ['.mjs', '.js', '.cjs'], + moduleTypes: loader, + resolve: { + extensions: ['.mjs', '.js', '.cjs'], + }, + plugins: [ + { + name: `angular-vite-optimize-deps${thirdPartySourcemaps ? '-vendor-sourcemap' : ''}`, + load: { + filter: { id: /\.[cm]?js$/ }, + async handler(id: string) { + const code = await prebundleTransformer.transformFile(id); + + return { code: Buffer.from(code).toString('utf-8') }; + }, + }, + }, + ], }, }; + + return config; } export interface DevServerExternalResultMetadata { diff --git a/packages/angular/build/src/utils/bundle-calculator.ts b/packages/angular/build/src/utils/bundle-calculator.ts index 3349a8a40830..71a75dafde02 100644 --- a/packages/angular/build/src/utils/bundle-calculator.ts +++ b/packages/angular/build/src/utils/bundle-calculator.ts @@ -152,6 +152,8 @@ function calculateSizes(budget: BudgetEntry, stats: BudgetStats): Size[] { } abstract class Calculator { + private assetMap?: ReadonlyMap; + constructor( protected budget: BudgetEntry, protected chunks: BudgetChunk[], @@ -167,15 +169,24 @@ abstract class Calculator { return 0; } + if (!this.assetMap) { + const map = new Map(); + for (const asset of this.assets) { + map.set(asset.name, asset.size); + } + this.assetMap = map; + } + const assetMap = this.assetMap; + return chunk.files .filter((file) => !file.endsWith('.map')) .map((file) => { - const asset = this.assets.find((asset) => asset.name === file); - if (!asset) { + const assetSize = assetMap.get(file); + if (assetSize === undefined) { throw new Error(`Could not find asset for file: ${file}`); } - return asset.size; + return assetSize; }) .reduce((l, r) => l + r, 0); } diff --git a/packages/angular/build/src/utils/debug-id.ts b/packages/angular/build/src/utils/debug-id.ts new file mode 100644 index 000000000000..a03fd571ec72 --- /dev/null +++ b/packages/angular/build/src/utils/debug-id.ts @@ -0,0 +1,106 @@ +/** + * @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 { createHash } from 'node:crypto'; + +/** + * Fixed RFC 4122 namespace UUID used to derive deterministic UUIDv5 build/Debug IDs. + * Treated as 16 raw bytes when fed into the SHA-1 of `namespace || name`. + * + * The exact value is arbitrary but must remain stable across releases so that + * the same source map content always yields the same Debug ID. + */ +const ANGULAR_BUILD_NAMESPACE = Buffer.from('6f9619ff8b86d011b42d00cf4fc964ff', 'hex'); + +/** + * Generates a deterministic UUIDv5 (RFC 4122 §4.3) Debug ID from the given name bytes. + * + * Determinism is recommended by the ECMA-426 "Source Map Debug ID" proposal + * (https://github.com/tc39/ecma426/blob/main/proposals/debug-id.md) so that the + * produced artifacts are stable across builds with the same source content. + * + * @param name Bytes that uniquely identify the artifact (typically the source map content). + * @returns A canonical UUIDv5 string (lowercase, hyphenated). + */ +export function generateDebugId(name: string | Uint8Array): string { + const sha = createHash('sha1').update(ANGULAR_BUILD_NAMESPACE).update(name).digest(); + + // Set version (5) in the high nibble of byte 6. + sha[6] = (sha[6] & 0x0f) | 0x50; + // Set RFC 4122 variant bits (10xx) in byte 8. + sha[8] = (sha[8] & 0x3f) | 0x80; + + const h = sha.toString('hex'); + + return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`; +} + +/** Pattern matching an existing `//# debugId=` comment at the end of the file. */ +const DEBUG_ID_COMMENT = /(\n\/\/# debugId=[^\r\n]*)(\n\/\/# sourceMappingURL=[^\r\n]*)?(\s*)$/; + +/** Pattern matching the `//# sourceMappingURL=` comment, used to position the debug-id line. */ +const SOURCE_MAPPING_URL_COMMENT = /\n\/\/# sourceMappingURL=[^\r\n]*\s*$/; + +/** + * Inserts (or replaces) a `//# debugId=` comment in the given JavaScript text. + * + * Per ECMA-426, the comment must appear within the last 5 lines and SHOULD be + * placed immediately above any `//# sourceMappingURL=` comment so that existing + * tools that only consult the final line still find the source-map URL. + */ +export function injectDebugIdIntoJs(text: string, id: string): string { + const comment = `//# debugId=${id}`; + + // Replace any existing debugId comment to keep the operation idempotent. + if (DEBUG_ID_COMMENT.test(text)) { + return text.replace(DEBUG_ID_COMMENT, (_, p1, p2, p3) => `\n${comment}${p2 || ''}${p3 || ''}`); + } + + if (SOURCE_MAPPING_URL_COMMENT.test(text)) { + return text.replace(SOURCE_MAPPING_URL_COMMENT, (match) => `\n${comment}${match}`); + } + + // No source map reference; append at the very end on its own line. + return text.endsWith('\n') ? `${text}${comment}\n` : `${text}\n${comment}\n`; +} + +/** + * Sets the top-level `debugId` field on a JSON source map. + * + * Per ECMA-426, source maps embed the same Debug ID under a `debugId` key so + * that consumers can pair a generated file with its source map without relying + * on URL/path conventions. + */ +export function injectDebugIdIntoSourceMap(json: string, id: string): string { + let parsed: Record; + try { + parsed = JSON.parse(json) as Record; + } catch { + // Source map is malformed; do not corrupt it further. + return json; + } + + if (parsed['debugId'] === id) { + return json; + } + + parsed['debugId'] = id; + + // Preserve existing pretty-print indentation when the source map is formatted. + const indent = json.match(/^[^{]*{\r?\n([ \t]+)/)?.[1]; + + return JSON.stringify(parsed, null, indent); +} + +/** + * Strips any existing `debugId` field from the source map JSON string to restore + * the original JSON contents for deterministic/idempotent hashing. + */ +export function stripDebugIdFromSourceMap(json: string): string { + return json.replace(/,\s*"debugId"\s*:\s*"[^"]*"|\s*"debugId"\s*:\s*"[^"]*"\s*,?/g, ''); +} diff --git a/packages/angular/build/src/utils/debug-id_spec.ts b/packages/angular/build/src/utils/debug-id_spec.ts new file mode 100644 index 000000000000..f254e28f6eee --- /dev/null +++ b/packages/angular/build/src/utils/debug-id_spec.ts @@ -0,0 +1,137 @@ +/** + * @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 { + generateDebugId, + injectDebugIdIntoJs, + injectDebugIdIntoSourceMap, + stripDebugIdFromSourceMap, +} from './debug-id'; + +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +describe('debug-id', () => { + describe('generateDebugId', () => { + it('produces a canonical UUIDv5 string', () => { + expect(generateDebugId('hello')).toMatch(UUID); + expect(generateDebugId(new TextEncoder().encode('hello'))).toMatch(UUID); + }); + + it('is deterministic for identical inputs', () => { + expect(generateDebugId('same content')).toBe(generateDebugId('same content')); + }); + + it('differs for different inputs', () => { + expect(generateDebugId('one')).not.toBe(generateDebugId('two')); + }); + }); + + describe('injectDebugIdIntoJs', () => { + const id = '11111111-2222-5333-9444-555555555555'; + + it('inserts the debugId comment immediately above sourceMappingURL', () => { + const text = 'console.log(1);\n//# sourceMappingURL=foo.js.map\n'; + const result = injectDebugIdIntoJs(text, id); + expect(result).toBe( + 'console.log(1);\n//# debugId=11111111-2222-5333-9444-555555555555\n//# sourceMappingURL=foo.js.map\n', + ); + }); + + it('appends the comment when no sourceMappingURL is present', () => { + const text = 'console.log(1);\n'; + const result = injectDebugIdIntoJs(text, id); + expect(result).toBe('console.log(1);\n//# debugId=11111111-2222-5333-9444-555555555555\n'); + }); + + it('appends a leading newline when input does not end with one', () => { + const text = 'console.log(1);'; + const result = injectDebugIdIntoJs(text, id); + expect(result).toBe('console.log(1);\n//# debugId=11111111-2222-5333-9444-555555555555\n'); + }); + + it('replaces an existing debugId comment (idempotent)', () => { + const original = + 'console.log(1);\n//# debugId=00000000-0000-5000-8000-000000000000\n//# sourceMappingURL=foo.js.map\n'; + const result = injectDebugIdIntoJs(original, id); + expect(result).toBe( + 'console.log(1);\n//# debugId=11111111-2222-5333-9444-555555555555\n//# sourceMappingURL=foo.js.map\n', + ); + // Re-running with the same id is a no-op. + expect(injectDebugIdIntoJs(result, id)).toBe(result); + }); + + it('does not replace a debugId comment that is inside a string or template literal', () => { + const text = + 'const t = `\n//# debugId=00000000-0000-5000-8000-000000000000\n`;\n//# sourceMappingURL=foo.js.map\n'; + const result = injectDebugIdIntoJs(text, id); + expect(result).toBe( + 'const t = `\n//# debugId=00000000-0000-5000-8000-000000000000\n`;\n' + + `//# debugId=${id}\n` + + '//# sourceMappingURL=foo.js.map\n', + ); + }); + }); + + describe('injectDebugIdIntoSourceMap', () => { + const id = '11111111-2222-5333-9444-555555555555'; + + it('adds a top-level debugId field', () => { + const map = JSON.stringify({ version: 3, sources: ['a.ts'], mappings: '' }); + const updated = JSON.parse(injectDebugIdIntoSourceMap(map, id)); + expect(updated.debugId).toBe(id); + expect(updated.version).toBe(3); + }); + + it('overwrites an existing debugId field', () => { + const map = JSON.stringify({ version: 3, debugId: 'old', mappings: '' }); + const updated = JSON.parse(injectDebugIdIntoSourceMap(map, id)); + expect(updated.debugId).toBe(id); + }); + + it('returns the original input when debugId already matches', () => { + const map = + '{\n "version": 3,\n "debugId": "11111111-2222-5333-9444-555555555555",\n "mappings": ""\n}'; + expect(injectDebugIdIntoSourceMap(map, id)).toBe(map); + }); + + it('preserves indentation when updating a pretty-printed source map', () => { + const map = '{\n\t"version": 3,\n\t"mappings": ""\n}'; + expect(injectDebugIdIntoSourceMap(map, id)).toBe( + '{\n\t"version": 3,\n\t"mappings": "",\n\t"debugId": "11111111-2222-5333-9444-555555555555"\n}', + ); + }); + + it('returns the original input when JSON is malformed', () => { + const malformed = '{ this is not json'; + expect(injectDebugIdIntoSourceMap(malformed, id)).toBe(malformed); + }); + }); + + describe('stripDebugIdFromSourceMap', () => { + it('removes the debugId field from pretty-printed source maps', () => { + const original = '{\n\t"version": 3,\n\t"mappings": ""\n}'; + const updated = + '{\n\t"version": 3,\n\t"mappings": "",\n\t"debugId": "11111111-2222-5333-9444-555555555555"\n}'; + expect(stripDebugIdFromSourceMap(updated)).toBe(original); + }); + + it('removes the debugId field from minified source maps', () => { + const original = '{"version":3,"mappings":""}'; + const updated = + '{"version":3,"mappings":"","debugId":"11111111-2222-5333-9444-555555555555"}'; + expect(stripDebugIdFromSourceMap(updated)).toBe(original); + }); + + it('removes the debugId field when it is the first property', () => { + const original = '{\n\t"version": 3,\n\t"mappings": ""\n}'; + const updated = + '{\n\t"debugId": "11111111-2222-5333-9444-555555555555",\n\t"version": 3,\n\t"mappings": ""\n}'; + expect(stripDebugIdFromSourceMap(updated)).toBe(original); + }); + }); +}); diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index b6a486f8f528..206467e929a0 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -106,7 +106,7 @@ export const allowMinify = debugOptimize.minify; * Allows using Rolldown for chunk optimization instead of Rollup. * This is useful for debugging and testing scenarios. */ -export const useRolldownChunks = parseTristate(process.env['NG_BUILD_CHUNKS_ROLLDOWN']) ?? false; +export const useRolldownChunks = parseTristate(process.env['NG_BUILD_CHUNKS_ROLLDOWN']) ?? true; /** * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. @@ -194,6 +194,13 @@ export const useComponentTemplateHmr = parseTristate(process.env['NG_HMR_TEMPLAT */ export const usePartialSsrBuild = parseTristate(process.env['NG_BUILD_PARTIAL_SSR']) === true; +/** + * When `NG_BUILD_BABEL_LINKER` is enabled (`1` or `true`), the Babel-based + * Angular Linker (`@angular/compiler-cli/linker/babel`) will be used instead of the + * default OXC in-place linker. + */ +export const useBabelLinker = parseTristate(process.env['NG_BUILD_BABEL_LINKER']) === true; + const bazelBinDirectory = process.env['BAZEL_BINDIR']; const bazelExecRoot = process.env['JS_BINARY__EXECROOT']; @@ -201,3 +208,19 @@ export const bazelEsbuildPluginPath = bazelBinDirectory && bazelExecRoot ? process.env['NG_INTERNAL_ESBUILD_PLUGINS_DO_NOT_USE'] : undefined; + +/** + * The persistent cache store configuration to use. + * Managed by the `NG_BUILD_CACHE_STORE` environment variable. + * - 'lmdb': Forces the use of LMDB. + * - 'sqlite': Forces the use of SQLite. + * - undefined / 'auto' / other: Automatically uses LMDB and falls back to SQLite. + */ +export const persistentCacheStoreSetting = (() => { + const env = process.env['NG_BUILD_CACHE_STORE']?.trim().toLowerCase(); + if (env === 'lmdb' || env === 'sqlite') { + return env; + } + + return undefined; +})(); diff --git a/packages/angular/build/src/utils/hash.ts b/packages/angular/build/src/utils/hash.ts new file mode 100644 index 000000000000..fdc5565afbb1 --- /dev/null +++ b/packages/angular/build/src/utils/hash.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 { XXHashAPI } from 'xxhash-wasm'; + +let xxhashInstance: XXHashAPI | undefined; +let xxhashPromise: Promise | undefined; + +/** + * Initializes the xxHash WASM instance early to ensure synchronous hashing uses xxHash. + */ +export async function initializeHash(): Promise { + if (xxhashInstance) { + return; + } + + xxhashPromise ??= import('xxhash-wasm').then((m) => m.default()); + xxhashInstance = await xxhashPromise; +} + +function getXxhash(): XXHashAPI { + assert( + xxhashInstance, + 'Hash utility must be initialized by awaiting `initializeHash()` before use.', + ); + + return xxhashInstance; +} + +/** + * Calculates a fast 64-bit non-cryptographic hash of the provided content. + * Suitable for cache keys, ETags, and change detection. + */ +export function calculateHash(data: string | Uint8Array): string { + const instance = getXxhash(); + + if (typeof data === 'string') { + return instance.h64ToString(data); + } + + return instance.h64Raw(data).toString(16).padStart(16, '0'); +} + +export interface ContentHasher { + update(data: string | Uint8Array): ContentHasher; + digest(): string; +} + +/** + * Creates a streaming 64-bit non-cryptographic content hasher. + */ +export function createContentHash(): ContentHasher { + const instance = getXxhash(); + const hasher = instance.create64(); + + const contentHasher: ContentHasher = { + update(data: string | Uint8Array): ContentHasher { + hasher.update(data); + + return contentHasher; + }, + digest(): string { + return hasher.digest().toString(16).padStart(16, '0'); + }, + }; + + return contentHasher; +} diff --git a/packages/angular/build/src/utils/hash_spec.ts b/packages/angular/build/src/utils/hash_spec.ts new file mode 100644 index 000000000000..77fcbc909403 --- /dev/null +++ b/packages/angular/build/src/utils/hash_spec.ts @@ -0,0 +1,60 @@ +/** + * @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 { calculateHash, createContentHash, initializeHash } from './hash'; + +describe('hash utility', () => { + beforeAll(async () => { + await initializeHash(); + }); + + it('should calculate identical 64-bit hex hash for string and Buffer with same content', () => { + const text = 'export const message = "hello world";'; + const buffer = Buffer.from(text, 'utf-8'); + + const stringHash = calculateHash(text); + const bufferHash = calculateHash(buffer); + + expect(typeof stringHash).toBe('string'); + expect(stringHash.length).toBe(16); + expect(stringHash).toBe(bufferHash); + }); + + it('should calculate different hashes for different contents', () => { + const hash1 = calculateHash('const a = 1;'); + const hash2 = calculateHash('const a = 2;'); + + expect(hash1).not.toBe(hash2); + }); + + it('should support streaming multi-part hashing matching combined single-shot hash', () => { + const part1 = 'header: '; + const part2 = 'body content: '; + const part3 = 'footer'; + + const hasher = createContentHash(); + hasher.update(part1); + hasher.update(part2); + hasher.update(Buffer.from(part3, 'utf-8')); + const streamingHash = hasher.digest(); + + const singleShotHash = calculateHash(part1 + part2 + part3); + + expect(streamingHash.length).toBe(16); + expect(streamingHash).toBe(singleShotHash); + }); + + it('should handle Uint8Array chunks in streaming hasher', () => { + const hasher = createContentHash(); + hasher.update(new Uint8Array([1, 2, 3, 4])).update('some-string'); + const digest = hasher.digest(); + + expect(typeof digest).toBe('string'); + expect(digest.length).toBe(16); + }); +}); diff --git a/packages/angular/build/src/utils/i18n-options.ts b/packages/angular/build/src/utils/i18n-options.ts index 822683bef03d..6a288622d053 100644 --- a/packages/angular/build/src/utils/i18n-options.ts +++ b/packages/angular/build/src/utils/i18n-options.ts @@ -28,6 +28,7 @@ export interface I18nOptions { flatOutput?: boolean; readonly shouldInline: boolean; hasDefinedSourceLocale?: boolean; + localizeVersion?: string; } function normalizeTranslationFileOption( 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 5e9d05d1bd56..becbee4be002 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 @@ -11,6 +11,12 @@ import { extname } from 'node:path'; import { htmlRewritingStream } from './html-rewriting-stream'; import { VALID_SELF_CLOSING_TAGS } from './valid-self-closing-tags'; +/** + * RegExp to check if a URL is resolvable. + * A URL is resolvable if it is absolute (starting with http/https) or relative (starting with `./`, `../`, or `/`). + */ +const RESOLVABLE_URL_REGEXP = /^(?:\.{0,2}\/|https?:\/\/)/i; + export type LoadOutputFileFunctionType = (file: string) => Promise; export type CrossOriginValue = 'none' | 'anonymous' | 'use-credentials'; @@ -168,7 +174,9 @@ export async function augmentIndexHtml( keyA.localeCompare(keyB), ); for (const [url, integrityHash] of sortedEntries) { - integrity[generateUrl(url, deployUrl)] = integrityHash; + const resolvedUrl = generateUrl(url, deployUrl); + const key = RESOLVABLE_URL_REGEXP.test(resolvedUrl) ? resolvedUrl : `./${resolvedUrl}`; + integrity[key] = integrityHash; } const importMapJson = JSON.stringify({ integrity }).replace(/${importMapJson}`; @@ -268,9 +276,11 @@ export async function augmentIndexHtml( if (isString(baseHref)) { updateAttribute(tag, 'href', baseHref); } + if (subResourceIntegrityTag) { rewriter.emitRaw(subResourceIntegrityTag); } + break; case 'link': if (readAttribute(tag, 'rel') === 'preconnect') { 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 f2801ab3202a..df292b8771a3 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 @@ -468,10 +468,10 @@ describe('augment-index-html', () => { const match = content.match(/`); @@ -166,12 +166,12 @@ describe('auto-csp', () => { // Loader script for main.js and main2.js appear after 'foo' and before 'bar'. expect(result).toMatch( // eslint-disable-next-line max-len - /console.log\('foo'\);<\/script>\s* +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', 'module', false, false, "sha384-xyz123", "anonymous"]];`, + ); + }); + + it('should preserve only integrity attribute when crossorigin is omitted', async () => { + const result = await autoCsp(` + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', '', false, false, "sha384-xyz123", null]];`, + ); + }); + + it('should map empty crossorigin attribute to anonymous', async () => { + const result = await autoCsp(` + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps).toHaveSize(1); + expect(csps[0]).toMatch(CSP_SINGLE_HASH_REGEX); + expect(result).toContain( + `const scripts = [['./main.js', '', false, false, null, "anonymous"]];`, + ); + }); }); diff --git a/packages/angular/build/src/utils/index-file/inline-critical-css.ts b/packages/angular/build/src/utils/index-file/inline-critical-css.ts index e5106f39cf6b..716d34bf037e 100644 --- a/packages/angular/build/src/utils/index-file/inline-critical-css.ts +++ b/packages/angular/build/src/utils/index-file/inline-critical-css.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import Beasties from 'beasties'; import { readFile } from 'node:fs/promises'; /** @@ -95,147 +94,184 @@ interface PartialDocument { querySelector(selector: string): PartialHTMLElement | null; } -/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ - -// We use Typescript declaration merging because `embedLinkedStylesheet` it's not declared in -// the `Beasties` types which means that we can't call the `super` implementation. interface BeastiesBase { embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise; + readFile(path: string): Promise; + process(html: string): Promise; } -class BeastiesBase extends Beasties {} -/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */ - -class BeastiesExtended extends BeastiesBase { - readonly warnings: string[] = []; - readonly errors: string[] = []; - private addedCspScriptsDocuments = new WeakSet(); - private documentNonces = new WeakMap(); - - constructor( - private readonly optionsExtended: InlineCriticalCssProcessorOptions & - InlineCriticalCssProcessOptions, - ) { - super({ - logger: { - warn: (s: string) => this.warnings.push(s), - error: (s: string) => this.errors.push(s), - info: () => {}, - }, - logLevel: 'warn', - path: optionsExtended.outputPath, - publicPath: optionsExtended.deployUrl, - compress: !!optionsExtended.minify, - pruneSource: false, - reduceInlineStyles: false, - mergeStylesheets: false, - // Note: if `preload` changes to anything other than `media`, the logic in - // `embedLinkedStylesheet` will have to be updated. - preload: 'media', - noscriptFallback: true, - inlineFonts: true, - }); - } - public override readFile(path: string): Promise { - const readAsset = this.optionsExtended.readAsset; +interface BeastiesExtendedInstance { + readonly warnings: string[]; + readonly errors: string[]; + process(html: string): Promise; +} - return readAsset ? readAsset(path) : readFile(path, 'utf-8'); +let beastiesClassPromise: + | Promise< + new ( + options: InlineCriticalCssProcessorOptions & InlineCriticalCssProcessOptions, + ) => BeastiesExtendedInstance + > + | undefined; + +async function getBeastiesClass(): Promise< + new ( + options: InlineCriticalCssProcessorOptions & InlineCriticalCssProcessOptions, + ) => BeastiesExtendedInstance +> { + if (beastiesClassPromise) { + return beastiesClassPromise; } - /** - * Override of the Beasties `embedLinkedStylesheet` method - * that makes it work with Angular's CSP APIs. - */ - override async embedLinkedStylesheet( - link: PartialHTMLElement, - document: PartialDocument, - ): Promise { - if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') { - // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64 - // NB: this is only needed for the webpack based builders. - const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); - if (media) { - link.removeAttribute('onload'); - link.setAttribute('media', media[1]); - link?.next?.remove(); - } - } - - const returnValue = await super.embedLinkedStylesheet(link, document); - const cspNonce = this.findCspNonce(document); - - if (cspNonce || this.optionsExtended.autoCsp) { - const beastiesMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); + beastiesClassPromise = import('beasties').then(({ default: Beasties }) => { + return class BeastiesExtended + extends (Beasties as unknown as new (options: unknown) => BeastiesBase) + implements BeastiesExtendedInstance + { + private _warnings?: string[]; + private _errors?: string[]; - if (beastiesMedia) { - // If there's a Beasties-generated `onload` handler and the file has an Angular CSP nonce, - // we have to remove the handler, because it's incompatible with CSP. We save the value - // in a different attribute and we generate a script tag with the nonce that uses - // `addEventListener` to apply the media query instead. - link.removeAttribute('onload'); - link.setAttribute(CSP_MEDIA_ATTR, beastiesMedia[1]); - this.conditionallyInsertCspLoadingScript(document, cspNonce, link); + get warnings(): string[] { + return (this._warnings ??= []); } - // Ideally we would hook in at the time Beasties inserts the `style` tags, but there isn't - // a way of doing that at the moment so we fall back to doing it any time a `link` tag is - // inserted. We mitigate it by only iterating the direct children of the `` which - // should be pretty shallow. - if (cspNonce) { - document.head.children.forEach((child) => { - if (child.tagName === 'style' && !child.hasAttribute('nonce')) { - child.setAttribute('nonce', cspNonce); - } + get errors(): string[] { + return (this._errors ??= []); + } + private addedCspScriptsDocuments = new WeakSet(); + private documentNonces = new WeakMap(); + + constructor( + private readonly optionsExtended: InlineCriticalCssProcessorOptions & + InlineCriticalCssProcessOptions, + ) { + super({ + logger: { + warn: (s: string) => this.warnings.push(s), + error: (s: string) => this.errors.push(s), + info: () => {}, + }, + logLevel: 'warn', + path: optionsExtended.outputPath, + publicPath: optionsExtended.deployUrl, + compress: !!optionsExtended.minify, + pruneSource: false, + reduceInlineStyles: false, + mergeStylesheets: false, + // Note: if `preload` changes to anything other than `media`, the logic in + // `embedLinkedStylesheet` will have to be updated. + preload: 'media', + noscriptFallback: true, + inlineFonts: true, }); } - } - return returnValue; - } + public override readFile(path: string): Promise { + const readAsset = this.optionsExtended.readAsset; - /** - * Finds the CSP nonce for a specific document. - */ - private findCspNonce(document: PartialDocument): string | null { - if (this.documentNonces.has(document)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.documentNonces.get(document)!; - } + return readAsset ? readAsset(path) : readFile(path, 'utf-8'); + } - // HTML attribute are case-insensitive, but the parser used by Beasties is case-sensitive. - const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]'); - const cspNonce = - nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null; + /** + * Override of the Beasties `embedLinkedStylesheet` method + * that makes it work with Angular's CSP APIs. + */ + override async embedLinkedStylesheet( + link: PartialHTMLElement, + document: PartialDocument, + ): Promise { + if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') { + // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64 + // NB: this is only needed for the webpack based builders. + const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); + if (media) { + link.removeAttribute('onload'); + link.setAttribute('media', media[1]); + link?.next?.remove(); + } + } + + const returnValue = await super.embedLinkedStylesheet(link, document); + const cspNonce = this.findCspNonce(document); + + if (cspNonce || this.optionsExtended.autoCsp) { + const beastiesMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); + + if (beastiesMedia) { + // If there's a Beasties-generated `onload` handler and the file has an Angular CSP nonce, + // we have to remove the handler, because it's incompatible with CSP. We save the value + // in a different attribute and we generate a script tag with the nonce that uses + // `addEventListener` to apply the media query instead. + link.removeAttribute('onload'); + link.setAttribute(CSP_MEDIA_ATTR, beastiesMedia[1]); + this.conditionallyInsertCspLoadingScript(document, cspNonce, link); + } - this.documentNonces.set(document, cspNonce); + // Ideally we would hook in at the time Beasties inserts the `style` tags, but there isn't + // a way of doing that at the moment so we fall back to doing it any time a `link` tag is + // inserted. We mitigate it by only iterating the direct children of the `` which + // should be pretty shallow. + if (cspNonce) { + document.head.children.forEach((child) => { + if (child.tagName === 'style' && !child.hasAttribute('nonce')) { + child.setAttribute('nonce', cspNonce); + } + }); + } + } - return cspNonce; - } + return returnValue; + } - /** - * Inserts the `script` tag that swaps the critical CSS at runtime, - * if one hasn't been inserted into the document already. - */ - private conditionallyInsertCspLoadingScript( - document: PartialDocument, - nonce: string | null, - link: PartialHTMLElement, - ): void { - if (this.addedCspScriptsDocuments.has(document)) { - return; - } + /** + * Finds the CSP nonce for a specific document. + */ + private findCspNonce(document: PartialDocument): string | null { + if (this.documentNonces.has(document)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.documentNonces.get(document)!; + } + + // HTML attribute are case-insensitive, but the parser used by Beasties is case-sensitive. + const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]'); + const cspNonce = + nonceElement?.getAttribute('ngCspNonce') || + nonceElement?.getAttribute('ngcspnonce') || + null; + + this.documentNonces.set(document, cspNonce); + + return cspNonce; + } - const script = document.createElement('script'); - script.textContent = LINK_LOAD_SCRIPT_CONTENT; - if (nonce) { - script.setAttribute('nonce', nonce); - } + /** + * Inserts the `script` tag that swaps the critical CSS at runtime, + * if one hasn't been inserted into the document already. + */ + private conditionallyInsertCspLoadingScript( + document: PartialDocument, + nonce: string | null, + link: PartialHTMLElement, + ): void { + if (this.addedCspScriptsDocuments.has(document)) { + return; + } + + const script = document.createElement('script'); + script.textContent = LINK_LOAD_SCRIPT_CONTENT; + if (nonce) { + script.setAttribute('nonce', nonce); + } + + // Prepend the script to the head since it needs to + // run as early as possible, before the `link` tags. + document.head.insertBefore(script, link); + this.addedCspScriptsDocuments.add(document); + } + }; + }); - // Prepend the script to the head since it needs to - // run as early as possible, before the `link` tags. - document.head.insertBefore(script, link); - this.addedCspScriptsDocuments.add(document); - } + return beastiesClassPromise; } export class InlineCriticalCssProcessor { @@ -245,7 +281,8 @@ export class InlineCriticalCssProcessor { html: string, options: InlineCriticalCssProcessOptions, ): Promise<{ content: string; warnings: string[]; errors: string[] }> { - const beasties = new BeastiesExtended({ ...this.options, ...options }); + const BeastiesClass = await getBeastiesClass(); + const beasties = new BeastiesClass({ ...this.options, ...options }); const content = await beasties.process(html); return { diff --git a/packages/angular/build/src/utils/index.ts b/packages/angular/build/src/utils/index.ts index 1a7cb15cd9c3..9df32fc10568 100644 --- a/packages/angular/build/src/utils/index.ts +++ b/packages/angular/build/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './normalize-asset-patterns'; export * from './normalize-optimization'; export * from './normalize-source-maps'; export * from './load-proxy-config'; +export * from './source-map'; diff --git a/packages/angular/build/src/utils/normalize-cache.ts b/packages/angular/build/src/utils/normalize-cache.ts index f272f6a78e45..c2dc52514992 100644 --- a/packages/angular/build/src/utils/normalize-cache.ts +++ b/packages/angular/build/src/utils/normalize-cache.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { join, resolve } from 'node:path'; +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { dirname, isAbsolute, join, resolve } from 'node:path'; /** Version placeholder is replaced during the build process with actual package version */ const VERSION = '0.0.0-PLACEHOLDER'; @@ -39,6 +40,46 @@ function hasCacheMetadata(value: unknown): value is { cli: { cache: CacheMetadat ); } +function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): string { + if (isAbsolute(cachePathSetting)) { + return cachePathSetting; + } + + try { + // Find the git directory, walking up from workspaceRoot if necessary + let currentDir = workspaceRoot; + while (true) { + const gitPath = join(currentDir, '.git'); + if (existsSync(gitPath)) { + const stat = statSync(gitPath); + if (stat.isFile()) { + // Could be a git worktree (or submodule) + const content = readFileSync(gitPath, 'utf8'); + const match = /^gitdir:\s*(.+)$/m.exec(content); + if (match) { + const gitdir = resolve(currentDir, match[1].trim()); + const commondirPath = join(gitdir, 'commondir'); + if (existsSync(commondirPath)) { + // It's a git worktree + const commondir = readFileSync(commondirPath, 'utf8').trim(); + const commonGitDir = resolve(gitdir, commondir); + + return resolve(dirname(commonGitDir), cachePathSetting); + } + } + } + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + } catch {} + + return resolve(workspaceRoot, cachePathSetting); +} + export function normalizeCacheOptions( projectMetadata: unknown, worspaceRoot: string, @@ -65,7 +106,7 @@ export function normalizeCacheOptions( } } - const cacheBasePath = resolve(worspaceRoot, path); + const cacheBasePath = getCacheBasePath(worspaceRoot, path); return { enabled: cacheEnabled, diff --git a/packages/angular/build/src/utils/normalize-cache_spec.ts b/packages/angular/build/src/utils/normalize-cache_spec.ts new file mode 100644 index 000000000000..c8a72870d62a --- /dev/null +++ b/packages/angular/build/src/utils/normalize-cache_spec.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 { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { normalizeCacheOptions } from './normalize-cache'; + +describe('normalizeCacheOptions', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'angular-cache-spec-')); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + it('should resolve cache path relative to workspace root in a standard repository', async () => { + const workspaceRoot = join(tempDir, 'project'); + await mkdir(join(workspaceRoot, '.git'), { recursive: true }); + + const options = normalizeCacheOptions({}, workspaceRoot); + + expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache')); + }); + + it('should resolve cache path relative to main repository root in a git worktree', async () => { + const mainRepoRoot = join(tempDir, 'main-repo'); + const mainGitDir = join(mainRepoRoot, '.git'); + const worktreeRoot = join(tempDir, 'worktree'); + + // Create main repo structure + await mkdir(mainGitDir, { recursive: true }); + + // Create worktree folder and .git file pointing to the main repo's worktree metadata folder + const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1'); + await mkdir(worktreeMetadataDir, { recursive: true }); + await mkdir(worktreeRoot, { recursive: true }); + await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`); + + // Create the commondir file in the worktree metadata folder pointing back to the main .git dir + await writeFile(join(worktreeMetadataDir, 'commondir'), '../..'); + + const options = normalizeCacheOptions({}, worktreeRoot); + + expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache')); + }); + + it('should resolve cache path relative to workspace root in a git submodule', async () => { + const mainRepoRoot = join(tempDir, 'main-repo'); + const submoduleRoot = join(mainRepoRoot, 'submodule'); + + // Create main repo structure and submodule metadata folder + const submoduleGitDir = join(mainRepoRoot, '.git/modules/sub'); + await mkdir(submoduleGitDir, { recursive: true }); + await mkdir(submoduleRoot, { recursive: true }); + + // Create .git file in submodule pointing to the metadata folder + await writeFile(join(submoduleRoot, '.git'), `gitdir: ../.git/modules/sub`); + + // Submodules do NOT have a 'commondir' file. + const options = normalizeCacheOptions({}, submoduleRoot); + + expect(options.basePath).toBe(resolve(submoduleRoot, '.angular/cache')); + }); + + it('should resolve cache path relative to workspace root when there is no git repository', async () => { + const workspaceRoot = join(tempDir, 'project'); + await mkdir(workspaceRoot, { recursive: true }); + + const options = normalizeCacheOptions({}, workspaceRoot); + + expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache')); + }); +}); diff --git a/packages/angular/build/src/utils/path.ts b/packages/angular/build/src/utils/path.ts index e969d420e429..a236878d134e 100644 --- a/packages/angular/build/src/utils/path.ts +++ b/packages/angular/build/src/utils/path.ts @@ -50,3 +50,17 @@ export function isSubDirectory(parent: string, child: string): boolean { return relativePath !== '..' && !relativePath.startsWith('../') && !isAbsolute(relativePath); } + +/** + * Canonicalizes a file path by normalising Windows drive-letter casing to uppercase. + * + * @param pathString - The file path to canonicalize. + * @returns The canonicalized file path. + */ +export function canonicalizePath(pathString: string): string { + if (platform === 'win32' && /^[a-z]:/.test(pathString)) { + return pathString[0].toUpperCase() + pathString.slice(1); + } + + return pathString; +} diff --git a/packages/angular/build/src/utils/path_spec.ts b/packages/angular/build/src/utils/path_spec.ts index 84c7d8ac31c4..cd8ca3372433 100644 --- a/packages/angular/build/src/utils/path_spec.ts +++ b/packages/angular/build/src/utils/path_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { isSubDirectory } from './path'; +import { canonicalizePath, isSubDirectory } from './path'; describe('isSubDirectory', () => { it('should return true for a direct child', () => { @@ -39,3 +39,17 @@ describe('isSubDirectory', () => { expect(isSubDirectory('/foo/bar', '/foo/bar/..baz/qux')).toBeTrue(); }); }); + +describe('canonicalizePath', () => { + it('should return the path unmodified on POSIX systems', () => { + expect(canonicalizePath('/foo/bar/baz')).toBe('/foo/bar/baz'); + }); + + if (process.platform === 'win32') { + it('should uppercase Windows drive-letter casing', () => { + expect(canonicalizePath('c:/foo/bar')).toBe('C:/foo/bar'); + expect(canonicalizePath('d:\\foo\\bar')).toBe('D:\\foo\\bar'); + expect(canonicalizePath('C:/foo/bar')).toBe('C:/foo/bar'); + }); + } +}); diff --git a/packages/angular/build/src/utils/resolve-project.ts b/packages/angular/build/src/utils/resolve-project.ts new file mode 100644 index 000000000000..ad31825683c4 --- /dev/null +++ b/packages/angular/build/src/utils/resolve-project.ts @@ -0,0 +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 { createRequire } from 'node:module'; +import { join } from 'node:path'; + +/** + * Creates a module resolver function that is strictly scoped to the project root. + * This prevents module resolution from leaking the parent module context when executing inside virtual stores (like pnpm). + * + * @param projectRoot The root directory of the project. + * @returns A resolver function that takes a package name/path and returns its resolved path. + */ +export function createProjectResolver(projectRoot: string): (packageName: string) => string { + const projectRequire = createRequire(join(projectRoot, 'package.json')); + + return (packageName: string) => projectRequire.resolve(packageName, { paths: [projectRoot] }); +} diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index bd294b1e7e4b..1ee430a76333 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -7,8 +7,8 @@ */ import type { Metafile } from 'esbuild'; +import { Buffer } from 'node:buffer'; import { extname } from 'node:path'; -import { runInThisContext } from 'node:vm'; import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { type BuildOutputFile, @@ -174,9 +174,14 @@ export function generateAngularServerAppManifest( ), ); - // This is needed because JavaScript engines script parser convert `\r\n` to `\n` in template literals, - // which can result in an incorrect byte length. - const size = runInThisContext(`new TextEncoder().encode(\`${escapedContent}\`).byteLength`); + // JavaScript engine script parsers normalize `\r\n` (2 bytes in UTF-8) to `\n` (1 byte in UTF-8) in template literals. + // Subtracting the count of `\r\n` occurrences avoids allocating any temporary strings or match arrays for large assets. + let size = Buffer.byteLength(file.text); + let pos = file.text.indexOf('\r\n'); + while (pos !== -1) { + size--; + pos = file.text.indexOf('\r\n', pos + 2); + } serverAssets[file.path] = `{size: ${size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`; diff --git a/packages/angular/build/src/utils/source-map.ts b/packages/angular/build/src/utils/source-map.ts new file mode 100644 index 000000000000..4fa97dd222c1 --- /dev/null +++ b/packages/angular/build/src/utils/source-map.ts @@ -0,0 +1,405 @@ +/** + * @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 { EncodedSourceMap } from '@ampproject/remapping'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const SOURCEMAP_COMMENT_PREFIX = '//# sourceMappingURL='; +export const SOURCEMAP_COMMENT_BYTES = Buffer.from(SOURCEMAP_COMMENT_PREFIX); + +/** + * Checks for a single trailing `//# sourceMappingURL=` comment on a raw buffer. + * + * @param data The raw byte buffer to inspect. + * @returns An object containing the sliced code buffer and URL snippet if a single trailing comment exists, + * `null` if no sourcemap comment exists in the buffer, or `undefined` if multiple/non-trailing comments exist. + */ +export function findTrailingSourceMapComment( + data: Uint8Array, +): { code: Uint8Array; urlLine: string } | null | undefined { + const dataBuffer = Buffer.isBuffer(data) + ? data + : Buffer.from(data.buffer, data.byteOffset, data.byteLength); + + const firstIndex = dataBuffer.indexOf(SOURCEMAP_COMMENT_BYTES); + if (firstIndex === -1) { + return null; + } + + const lastIndex = dataBuffer.lastIndexOf(SOURCEMAP_COMMENT_BYTES); + // Skip any preceding horizontal whitespace (spaces/tabs) to find the start of the line. + let prevIdx = lastIndex - 1; + while (prevIdx >= 0 && (dataBuffer[prevIdx] === 32 || dataBuffer[prevIdx] === 9)) { + prevIdx--; + } + // Ensure the comment starts at the beginning of a line or the start of the file, + // preventing false positives for occurrences inside inline string literals or code. + const isLineStart = prevIdx < 0 || dataBuffer[prevIdx] === 10 || dataBuffer[prevIdx] === 13; + + if (firstIndex === lastIndex && isLineStart) { + const urlLine = dataBuffer + .subarray(lastIndex + SOURCEMAP_COMMENT_BYTES.length) + .toString('utf-8'); + + return { + code: dataBuffer.subarray(0, prevIdx < 0 ? 0 : prevIdx + 1), + urlLine, + }; + } + + return undefined; +} + +/** + * Removes `//# sourceMappingURL=` comments safely from the given JavaScript code, + * ignoring any occurrences that are inside string literals, template literals, or block comments. + * + * For raw Uint8Array / Buffer inputs, it optimizes performance by inspecting trailing byte sequences + * to slice the buffer directly without full string decoding. + * + * @param code The JavaScript source code as a string or Uint8Array. + * @returns The code with top-level sourcemap comments removed. + */ +export function removeSourceMappingURL(code: string): string; +export function removeSourceMappingURL(code: Uint8Array): Uint8Array; +export function removeSourceMappingURL(code: string | Uint8Array): string | Uint8Array { + if (typeof code === 'string') { + return removeSourceMappingURLFromString(code); + } + + const trailing = findTrailingSourceMapComment(code); + if (trailing === null) { + return code; + } + + if (trailing && isTrailingSourceMapComment(trailing.urlLine)) { + return trailing.code; + } + + // Fallback to full decode and state-machine stripping for multiple comments or non-trailing comments + const dataBuffer = Buffer.isBuffer(code) + ? code + : Buffer.from(code.buffer, code.byteOffset, code.byteLength); + const text = dataBuffer.toString('utf-8'); + const stripped = removeSourceMappingURLFromString(text); + + return stripped === text ? code : Buffer.from(stripped, 'utf-8'); +} + +function removeSourceMappingURLFromString(code: string): string { + if (!code.includes(SOURCEMAP_COMMENT_PREFIX)) { + return code; + } + + const result: string[] = []; + let lastCopiedIndex = 0; + let i = 0; + const len = code.length; + + // Stack to track template literal state and curly brace depth for nested interpolations. + const stack: { type: 'template'; braceDepth: number }[] = []; + let currentState: + 'normal' | 'string_double' | 'string_single' | 'template' | 'comment_block' | 'comment_line' = + 'normal'; + + while (i < len) { + const char = code[i]; + const nextChar = code[i + 1]; + + if (currentState === 'normal') { + if (char === '/' && nextChar === '*') { + currentState = 'comment_block'; + i += 2; + continue; + } + if (char === '/' && nextChar === '/') { + // Detect if the comment is escaped (e.g. inside a regex literal like `/\/\/#/`). + let isEscaped = false; + let prevIdx = i - 1; + while (prevIdx >= 0 && /\s/.test(code[prevIdx])) { + prevIdx--; + } + if (prevIdx >= 0 && code[prevIdx] === '\\') { + let bsCount = 0; + while (prevIdx >= 0 && code[prevIdx] === '\\') { + bsCount++; + prevIdx--; + } + if (bsCount % 2 === 1) { + isEscaped = true; + } + } + + if (!isEscaped && code.startsWith(SOURCEMAP_COMMENT_PREFIX, i)) { + if (i > lastCopiedIndex) { + result.push(code.slice(lastCopiedIndex, i)); + } + // Skip the rest of the comment line up to the newline + i += SOURCEMAP_COMMENT_PREFIX.length; + while (i < len && code[i] !== '\n' && code[i] !== '\r') { + i++; + } + lastCopiedIndex = i; + continue; + } else { + currentState = 'comment_line'; + i += 2; + continue; + } + } + if (char === '"') { + currentState = 'string_double'; + i++; + continue; + } + if (char === "'") { + currentState = 'string_single'; + i++; + continue; + } + if (char === '`') { + currentState = 'template'; + stack.push({ type: 'template', braceDepth: 0 }); + i++; + continue; + } + if (char === '{') { + const top = stack[stack.length - 1]; + if (top) { + top.braceDepth++; + } + i++; + continue; + } + if (char === '}') { + const top = stack[stack.length - 1]; + if (top) { + top.braceDepth--; + if (top.braceDepth < 0) { + // Exiting a template literal interpolation ${ ... } + stack.pop(); + currentState = 'template'; + i++; + continue; + } + } + i++; + continue; + } + + i++; + } else if (currentState === 'string_double') { + if (char === '\\') { + i += 2; + continue; + } + if (char === '"') { + currentState = 'normal'; + } + i++; + } else if (currentState === 'string_single') { + if (char === '\\') { + i += 2; + continue; + } + if (char === "'") { + currentState = 'normal'; + } + i++; + } else if (currentState === 'template') { + if (char === '\\') { + i += 2; + continue; + } + if (char === '$' && nextChar === '{') { + // Entering template literal interpolation context + currentState = 'normal'; + stack.push({ type: 'template', braceDepth: 0 }); + i += 2; + continue; + } + if (char === '`') { + stack.pop(); + currentState = 'normal'; + } + i++; + } else if (currentState === 'comment_block') { + if (char === '*' && nextChar === '/') { + currentState = 'normal'; + i += 2; + continue; + } + i++; + } else if (currentState === 'comment_line') { + if (char === '\n' || char === '\r') { + currentState = 'normal'; + } + i++; + } + } + + if (lastCopiedIndex === 0) { + return code; + } + + if (lastCopiedIndex < len) { + result.push(code.slice(lastCopiedIndex)); + } + + return result.join(''); +} + +/** + * Extracts the base64 payload from an inline sourcemap data URI line and verifies + * that only trailing whitespace follows the payload. + * + * @returns The base64 payload string if valid and trailing, or `undefined` otherwise. + */ +function extractTrailingBase64Payload(urlLine: string): string | undefined { + if (!urlLine.startsWith('data:application/json;')) { + return undefined; + } + + const base64StartIndex = urlLine.indexOf('base64,'); + if (base64StartIndex === -1) { + return undefined; + } + + const payloadStart = base64StartIndex + 7; + let payloadEnd = urlLine.length; + // Find the first trailing whitespace character that marks the end of the base64 payload. + for (let i = payloadStart; i < urlLine.length; i++) { + const char = urlLine[i]; + if (char === ' ' || char === '\r' || char === '\n' || char === '\t') { + payloadEnd = i; + break; + } + } + + // Verify that everything after the base64 payload is trailing whitespace + // to ensure this is a valid trailing sourceMappingURL comment at the end of the file. + for (let i = payloadEnd; i < urlLine.length; i++) { + const char = urlLine[i]; + if (char !== ' ' && char !== '\r' && char !== '\n' && char !== '\t') { + return undefined; + } + } + + return urlLine.slice(payloadStart, payloadEnd); +} + +/** + * Extracts the URL from an external sourcemap comment line and verifies + * that only trailing whitespace follows the URL. + * + * @returns The URL string if valid and trailing, or `undefined` otherwise. + */ +function extractTrailingUrl(urlLine: string): string | undefined { + if (urlLine.startsWith('data:')) { + return undefined; + } + + const urlMatch = /^([^\r\n\s'"`]+)/.exec(urlLine); + if (!urlMatch) { + return undefined; + } + + const remaining = urlLine.slice(urlMatch[1].length); + if (!/^\s*$/.test(remaining)) { + return undefined; + } + + return urlMatch[1]; +} + +/** + * Checks whether a `//# sourceMappingURL=` URL line snippet represents a valid trailing comment at the end of the file. + */ +export function isTrailingSourceMapComment(urlLine: string): boolean { + return ( + extractTrailingBase64Payload(urlLine) !== undefined || extractTrailingUrl(urlLine) !== undefined + ); +} + +/** + * Resolves and loads the input sourcemap referenced in a `//# sourceMappingURL=` URL line snippet. + * Supports inline base64 data URIs, local absolute file URLs, and relative/absolute filesystem paths. + */ +export function loadInputSourceMapFromUrl( + filename: string, + urlLine: string, +): EncodedSourceMap | undefined { + // Inline base64-encoded sourcemaps can be extremely large (up to megabytes). + // Parse them without regular expressions to avoid heavy backtracking and allocations. + const base64Payload = extractTrailingBase64Payload(urlLine); + if (base64Payload !== undefined) { + try { + return JSON.parse(Buffer.from(base64Payload, 'base64').toString('utf-8')) as EncodedSourceMap; + } catch { + return undefined; + } + } + + const url = extractTrailingUrl(urlLine); + if (!url) { + return undefined; + } + + if (url.startsWith('file://')) { + // Local absolute file URL scheme. + try { + const mapPath = fileURLToPath(url); + if (existsSync(mapPath)) { + return JSON.parse(readFileSync(mapPath, 'utf8')) as EncodedSourceMap; + } + } catch {} + } else if (!/^[a-z]+:\/\//i.test(url)) { + // Local relative or absolute filesystem path (percent-decoded as it originates from a URI). + try { + const mapPath = resolve(dirname(filename), decodeURIComponent(url)); + if (existsSync(mapPath)) { + return JSON.parse(readFileSync(mapPath, 'utf8')) as EncodedSourceMap; + } + } catch {} + } + + return undefined; +} + +/** + * Finds, resolves, and loads the input sourcemap referenced in the code's trailing + * sourceMappingURL comment, if present. Supports inline base64 data URIs, local absolute + * file URLs, and relative/absolute filesystem paths. + */ +export function loadInputSourceMap(filename: string, code: string): EncodedSourceMap | undefined { + // Locate the last sourceMappingURL comment using lastIndexOf to avoid scanning + // the entire file with a regular expression (significant for large files). + const lastSourceMapIndex = code.lastIndexOf(SOURCEMAP_COMMENT_PREFIX); + if (lastSourceMapIndex === -1) { + return undefined; + } + + if (lastSourceMapIndex > 0) { + // Skip any preceding horizontal whitespace (spaces/tabs) to find the start of the line. + let prevIdx = lastSourceMapIndex - 1; + while (prevIdx >= 0 && (code[prevIdx] === ' ' || code[prevIdx] === '\t')) { + prevIdx--; + } + // Ensure the comment starts at the beginning of a line, preventing false positives within code or strings. + if (prevIdx >= 0 && code[prevIdx] !== '\n' && code[prevIdx] !== '\r') { + return undefined; + } + } + + return loadInputSourceMapFromUrl( + filename, + code.slice(lastSourceMapIndex + SOURCEMAP_COMMENT_PREFIX.length), + ); +} diff --git a/packages/angular/build/src/utils/source-map_spec.ts b/packages/angular/build/src/utils/source-map_spec.ts new file mode 100644 index 000000000000..a5680d61fa4f --- /dev/null +++ b/packages/angular/build/src/utils/source-map_spec.ts @@ -0,0 +1,225 @@ +/** + * @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 { + isTrailingSourceMapComment, + loadInputSourceMap, + loadInputSourceMapFromUrl, + removeSourceMappingURL, +} from './source-map'; + +describe('removeSourceMappingURL', () => { + it('should remove top-level sourcemap comments', () => { + const code = 'console.log("hello");\n//# sourceMappingURL=main.js.map'; + expect(removeSourceMappingURL(code)).toBe('console.log("hello");\n'); + }); + + it('should not remove sourcemap comments inside double-quoted strings', () => { + const code = 'const str = "//# sourceMappingURL=inline.js.map";'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside single-quoted strings', () => { + const code = "const str = '//# sourceMappingURL=inline.js.map';"; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside template literals', () => { + const code = 'const str = `\n//# sourceMappingURL=inline.js.map\n`;'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside block comments', () => { + const code = '/*\n//# sourceMappingURL=inline.js.map\n*/'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside normal single-line comments', () => { + const code = '// Some description of //# sourceMappingURL=inline.js.map'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should remove multiple top-level sourcemap comments', () => { + const code = + '//# sourceMappingURL=first.js.map\nconsole.log("mid");\n//# sourceMappingURL=second.js.map'; + expect(removeSourceMappingURL(code)).toBe('\nconsole.log("mid");\n'); + }); + + it('should not remove sourcemap comments inside strings containing escaped quotes', () => { + const codeDouble = 'const str = "escaped \\" //# sourceMappingURL=inline.js.map";'; + expect(removeSourceMappingURL(codeDouble)).toBe(codeDouble); + + const codeSingle = "const str = 'escaped \\' //# sourceMappingURL=inline.js.map';"; + expect(removeSourceMappingURL(codeSingle)).toBe(codeSingle); + }); + + it('should handle strings containing escaped backslashes correctly', () => { + const code = 'const str = "backslash \\\\";\n//# sourceMappingURL=main.js.map'; + expect(removeSourceMappingURL(code)).toBe('const str = "backslash \\\\";\n'); + }); + + it('should not remove sourcemap comments inside template literal interpolations', () => { + const code = 'const str = `hello ${"//# sourceMappingURL=inline.js.map"} world`;'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside nested template literals', () => { + const code = 'const str = `nested ${`inner ${"//# sourceMappingURL=inline.js.map"}`}`;'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside nested template literals without inner quotes', () => { + const code = 'const str = `nested ${`inner //# sourceMappingURL=inline.js.map`}`;'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not remove sourcemap comments inside regex literals', () => { + const code = 'const regex = /\\/\\/# sourceMappingURL=inline.js.map/;'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should not affect normal division operators', () => { + const code = 'const ratio = 10 / 2;\n//# sourceMappingURL=main.js.map'; + expect(removeSourceMappingURL(code)).toBe('const ratio = 10 / 2;\n'); + }); + + it('should only remove exact sourceMappingURL prefix comments', () => { + const code = '// # sourceMappingURL=main.js.map\n//# sourceMappingURL=main.js.map'; + expect(removeSourceMappingURL(code)).toBe('// # sourceMappingURL=main.js.map\n'); + }); + + it('should return the exact input string when no sourcemap comment is present', () => { + const code = 'const x = 1;\nconsole.log(x);'; + expect(removeSourceMappingURL(code)).toBe(code); + }); + + it('should handle sourcemap comments with CRLF newlines', () => { + const code = 'console.log("hello");\r\n//# sourceMappingURL=main.js.map\r\nconst next = 2;'; + expect(removeSourceMappingURL(code)).toBe('console.log("hello");\r\n\r\nconst next = 2;'); + }); + + describe('with Uint8Array / Buffer inputs', () => { + it('should strip trailing sourcemap comment from Uint8Array buffer', () => { + const buffer = Buffer.from( + 'console.log("hello");\n//# sourceMappingURL=main.js.map', + 'utf-8', + ); + const result = removeSourceMappingURL(buffer); + + expect(Buffer.from(result).toString('utf-8')).toBe('console.log("hello");\n'); + }); + + it('should return exact input buffer when no sourcemap comment is present', () => { + const buffer = Buffer.from('console.log("hello");\nconst x = 1;', 'utf-8'); + const result = removeSourceMappingURL(buffer); + + expect(result).toBe(buffer); + }); + + it('should handle multiple sourcemap comments in a buffer via fallback', () => { + const buffer = Buffer.from( + '//# sourceMappingURL=first.js.map\nconsole.log("mid");\n//# sourceMappingURL=second.js.map', + 'utf-8', + ); + const result = removeSourceMappingURL(buffer); + + expect(Buffer.from(result).toString('utf-8')).toBe('\nconsole.log("mid");\n'); + }); + + it('should not strip sourcemap comments inside template strings in a buffer', () => { + const buffer = Buffer.from('const str = `\n//# sourceMappingURL=inline.js.map\n`;', 'utf-8'); + const result = removeSourceMappingURL(buffer); + + expect(Buffer.from(result).toString('utf-8')).toBe(buffer.toString('utf-8')); + }); + }); +}); + +describe('loadInputSourceMapFromUrl', () => { + it('should decode inline base64 sourcemaps', () => { + const map = { version: 3, sources: ['foo.ts'], mappings: 'AAAA;' }; + const base64 = Buffer.from(JSON.stringify(map)).toString('base64'); + const urlLine = `data:application/json;charset=utf-8;base64,${base64}\n`; + + expect(loadInputSourceMapFromUrl('/src/foo.js', urlLine)).toEqual(map as never); + }); + + it('should return undefined for invalid base64 payloads', () => { + expect( + loadInputSourceMapFromUrl('/src/foo.js', 'data:application/json;base64,invalid!!!'), + ).toBeUndefined(); + }); + + it('should return undefined when no base64 marker is found in data URI', () => { + expect( + loadInputSourceMapFromUrl('/src/foo.js', 'data:application/json;utf8,{}'), + ).toBeUndefined(); + }); +}); + +describe('loadInputSourceMap', () => { + it('should extract and decode sourcemap from source string', () => { + const map = { version: 3, sources: ['foo.ts'], mappings: 'AAAA;' }; + const base64 = Buffer.from(JSON.stringify(map)).toString('base64'); + const code = `console.log("hello");\n//# sourceMappingURL=data:application/json;base64,${base64}\n`; + + expect(loadInputSourceMap('/src/foo.js', code)).toEqual(map as never); + }); + + it('should return undefined when no sourceMappingURL comment is present', () => { + expect(loadInputSourceMap('/src/foo.js', 'console.log("hello");')).toBeUndefined(); + }); + + it('should return undefined for comments inside template strings with code after them', () => { + const map = { version: 3, sources: ['foo.ts'], mappings: 'AAAA;' }; + const base64 = Buffer.from(JSON.stringify(map)).toString('base64'); + const code = `const str = \`\n//# sourceMappingURL=data:application/json;base64,${base64}\n\`;\nconsole.log(str);`; + + expect(loadInputSourceMap('/src/foo.js', code)).toBeUndefined(); + }); + + it('should return undefined for comments inside template strings ending with backticks', () => { + const map = { version: 3, sources: ['foo.ts'], mappings: 'AAAA;' }; + const base64 = Buffer.from(JSON.stringify(map)).toString('base64'); + const code = `const str = \`\n//# sourceMappingURL=data:application/json;base64,${base64}\n\`;`; + + expect(loadInputSourceMap('/src/foo.js', code)).toBeUndefined(); + }); + + it('should return undefined for single-line template literals', () => { + const map = { version: 3, sources: ['foo.ts'], mappings: 'AAAA;' }; + const base64 = Buffer.from(JSON.stringify(map)).toString('base64'); + const code = `const str = \`//# sourceMappingURL=data:application/json;base64,${base64}\`;`; + + expect(loadInputSourceMap('/src/foo.js', code)).toBeUndefined(); + }); +}); + +describe('isTrailingSourceMapComment', () => { + it('should return true for valid inline data URIs at end of file', () => { + const map = { version: 3, sources: ['foo.ts'], mappings: 'AAAA;' }; + const base64 = Buffer.from(JSON.stringify(map)).toString('base64'); + const urlLine = `data:application/json;charset=utf-8;base64,${base64}\n`; + + expect(isTrailingSourceMapComment(urlLine)).toBe(true); + }); + + it('should return true for external sourcemap URLs at end of file', () => { + expect(isTrailingSourceMapComment('main.js.map\n')).toBe(true); + expect(isTrailingSourceMapComment('main.js.map')).toBe(true); + }); + + it('should return false when followed by non-whitespace characters', () => { + expect(isTrailingSourceMapComment('main.js.map\n`;\nconsole.log("hi");')).toBe(false); + expect(isTrailingSourceMapComment('main.js.map`;')).toBe(false); + }); + + it('should return false for invalid data URI format', () => { + expect(isTrailingSourceMapComment('data:application/json;utf8,{}')).toBe(false); + }); +}); diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index eed9ad7360f1..1eb48f2fc6f2 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -29,7 +29,6 @@ RUNTIME_ASSETS = glob( include = [ "bin/**/*", "src/**/*.md", - "src/**/*.json", ], exclude = [ "lib/config/workspace-schema.json", @@ -53,7 +52,6 @@ ts_project( ) + [ # These files are generated from the JSON schema "//packages/angular/cli:lib/config/workspace-schema.ts", - "//packages/angular/cli:src/commands/update/schematic/schema.ts", ], data = RUNTIME_ASSETS, deps = [ @@ -62,25 +60,18 @@ ts_project( ":node_modules/@angular-devkit/schematics", ":node_modules/@inquirer/prompts", ":node_modules/@listr2/prompt-adapter-inquirer", - ":node_modules/@modelcontextprotocol/sdk", - ":node_modules/@yarnpkg/lockfile", - ":node_modules/algoliasearch", - ":node_modules/ini", + ":node_modules/@modelcontextprotocol/server", ":node_modules/jsonc-parser", ":node_modules/listr2", ":node_modules/npm-package-arg", - ":node_modules/pacote", ":node_modules/parse5-html-rewriting-stream", ":node_modules/yargs", ":node_modules/zod", "//:node_modules/@angular/core", - "//:node_modules/@types/ini", "//:node_modules/@types/node", "//:node_modules/@types/npm-package-arg", - "//:node_modules/@types/pacote", "//:node_modules/@types/semver", "//:node_modules/@types/yargs", - "//:node_modules/@types/yarnpkg__lockfile", "//:node_modules/semver", "//:node_modules/typescript", ], @@ -105,11 +96,6 @@ ts_json_schema( data = CLI_SCHEMA_DATA, ) -ts_json_schema( - name = "update_schematic_schema", - src = "src/commands/update/schematic/schema.json", -) - ts_project( name = "angular-cli_test_lib", testonly = True, @@ -127,7 +113,7 @@ ts_project( ":angular-cli", ":node_modules/@angular-devkit/core", ":node_modules/@angular-devkit/schematics", - ":node_modules/@modelcontextprotocol/sdk", + ":node_modules/@modelcontextprotocol/server", ":node_modules/yargs", "//:node_modules/@types/semver", "//:node_modules/@types/yargs", diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index cd324b6df69b..97d410890955 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -10,8 +10,6 @@ import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import * as path from 'node:path'; import { SemVer, major } from 'semver'; -import { colors } from '../src/utilities/color'; -import { isWarningEnabled } from '../src/utilities/config'; import { disableVersionCheck } from '../src/utilities/environment-options'; import { VERSION } from '../src/utilities/version'; @@ -24,6 +22,26 @@ import { VERSION } from '../src/utilities/version'; let forceExit = false; (async (): Promise => { + /** + * On Windows, ensure process.cwd() uses uppercase drive letter casing. + * Windows cmd.exe and VS Code terminals often preserve lowercase drive letters (e.g. c:\...), + * which causes ESM module duplication in Node.js (e.g. loading "vitest" twice) and path + * lookup mismatches in tools like Vite and esbuild. + */ + if (process.platform === 'win32') { + const cwd = process.cwd(); + if (/^[a-z]:/.test(cwd)) { + try { + process.chdir(cwd[0].toUpperCase() + cwd.slice(1)); + } catch { + // Ignore failure to change directory + } + } + + // Ensure Windows CreateProcess does not search the current directory for bare executable names. + process.env['NoDefaultCurrentDirectoryInExePath'] = '1'; + } + /** * Disable Browserslist old data warning as otherwise with every release we'd need to update this dependency * which is cumbersome considering we pin versions and the warning is not user actionable. @@ -31,6 +49,7 @@ let forceExit = false; * See: https://github.com/browserslist/browserslist/blob/819c4337456996d19db6ba953014579329e9c6e1/node.js#L324 */ process.env.BROWSERSLIST_IGNORE_OLD_DATA = '1'; + const rawCommandName = process.argv[2]; /** @@ -104,15 +123,23 @@ let forceExit = false; cli.VERSION.major - globalVersion.major <= 1 ) { cli = await import('./cli'); - } else if (await isWarningEnabled('versionMismatch')) { - // Otherwise, use local version and warn if global is newer than local - const warning = - `Your global Angular CLI version (${globalVersion}) is greater than your local ` + - `version (${localVersion}). The local Angular CLI version is used.\n\n` + - 'To disable this warning use "ng config -g cli.warnings.versionMismatch false".'; + } else { + try { + const { isWarningEnabled } = await import('../src/utilities/config'); + if (await isWarningEnabled('versionMismatch')) { + // Otherwise, use local version and warn if global is newer than local + const warning = + `Your global Angular CLI version (${globalVersion}) is greater than your local ` + + `version (${localVersion}). The local Angular CLI version is used.\n\n` + + 'To disable this warning use "ng config -g cli.warnings.versionMismatch false".'; - // eslint-disable-next-line no-console - console.error(colors.yellow(warning)); + const { colors } = await import('../src/utilities/color'); + // eslint-disable-next-line no-console + console.error(colors.yellow(warning)); + } + } catch { + // Ignore errors during warning check to avoid falling back to global CLI + } } } } catch { diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 4488b768601b..4840834b4bed 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -16,19 +16,15 @@ "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", "@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER", "@inquirer/prompts": "8.5.2", - "@listr2/prompt-adapter-inquirer": "4.2.4", - "@modelcontextprotocol/sdk": "1.29.0", + "@listr2/prompt-adapter-inquirer": "4.2.5", + "@modelcontextprotocol/server": "2.0.0", "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", - "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.53.0", - "ini": "7.0.0", "jsonc-parser": "3.3.1", - "listr2": "10.2.1", + "listr2": "11.0.0", "npm-package-arg": "14.0.0", - "pacote": "21.5.0", "parse5-html-rewriting-stream": "8.0.1", - "semver": "7.8.1", - "yargs": "18.0.0", + "semver": "7.8.5", + "yargs": "18.1.0", "zod": "4.4.3" }, "ng-update": { diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index b0d15d70d0b2..ae0eb82f0cb5 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -14,6 +14,7 @@ import { Parser as yargsParser } from 'yargs/helpers'; import { getAnalyticsUserId } from '../analytics/analytics'; import { AnalyticsCollector } from '../analytics/analytics-collector'; import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters'; +import { PackageManagerError } from '../package-managers'; import { considerSettingUpAutocompletion } from '../utilities/completion'; import { AngularWorkspace } from '../utilities/config'; import { memoize } from '../utilities/memoize'; @@ -95,6 +96,7 @@ export abstract class CommandModule implements CommandModuleI async handler(args: ArgumentsCamelCase & OtherOptions): Promise { const { _, $0, ...options } = args; + const { logger } = this.context; // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. const camelCasedOptions: Record = {}; @@ -103,10 +105,7 @@ export abstract class CommandModule implements CommandModuleI } // Set up autocompletion if appropriate. - const autocompletionExitCode = await considerSettingUpAutocompletion( - this.commandName, - this.context.logger, - ); + const autocompletionExitCode = await considerSettingUpAutocompletion(this.commandName, logger); if (autocompletionExitCode !== undefined) { process.exitCode = autocompletionExitCode; @@ -127,7 +126,13 @@ export abstract class CommandModule implements CommandModuleI exitCode = await this.run(camelCasedOptions as Options & OtherOptions); } catch (e) { if (e instanceof schema.SchemaValidationException) { - this.context.logger.fatal(`Error: ${e.message}`); + logger.fatal(`Error: ${e.message}`); + exitCode = 1; + } else if (e instanceof PackageManagerError) { + const output = e.stderr || e.stdout; + logger.fatal( + `Error: Package installation failed: ${e.message}${output ? `\nOutput: ${output}` : ''}`, + ); exitCode = 1; } else { throw e; diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index dd78b7038120..bc95f33ff8e7 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -25,12 +25,7 @@ import { SchematicsCommandArgs, SchematicsCommandModule, } from '../../command-builder/schematics-command-module'; -import { - NgAddSaveDependency, - PackageManagerError, - PackageManifest, - PackageMetadata, -} from '../../package-managers'; +import { NgAddSaveDependency, PackageManifest, PackageMetadata } from '../../package-managers'; import { assertIsError } from '../../utilities/error'; import { isTTY } from '../../utilities/tty'; import { VERSION } from '../../utilities/version'; @@ -248,9 +243,7 @@ export default class AddCommandModule }, // TODO: Rework schematic execution as a task and insert here ], - { - /* options */ - }, + {/* options */}, ); try { @@ -268,6 +261,9 @@ export default class AddCommandModule if (localManifest['ng-add']?.save === false) { shouldCleanUp = true; } + } else { + await this.cleanUpTemporaryDependency(result.collectionName); + shouldCleanUp = false; } } catch {} } @@ -639,47 +635,36 @@ export default class AddCommandModule // Only show if installation will actually occur task.title = 'Installing package'; - try { - 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 { workingDirectory } = await packageManager.acquireTempPackage( - packageIdentifier.toString(), - { - registry, - }, - ); + if (context.savePackage === false) { + task.title += ' in temporary location'; - const tempRequire = createRequire(workingDirectory + '/'); - assert(context.collectionName, 'Collection name should always be available'); - const resolvedCollectionPath = tempRequire.resolve( - join(context.collectionName, 'package.json'), - ); + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const { workingDirectory } = await packageManager.acquireTempPackage( + packageIdentifier.toString(), + { + registry, + }, + ); - context.collectionName = dirname(resolvedCollectionPath); - } else { - await packageManager.add( - packageIdentifier.toString(), - 'none', - savePackage === 'devDependencies', - false, - true, - { - registry, - }, - ); - } - } catch (e) { - if (e instanceof PackageManagerError) { - const output = e.stderr || e.stdout; - if (output) { - throw new CommandError(`Package installation failed: ${e.message}\nOutput: ${output}`); - } - } + const tempRequire = createRequire(workingDirectory + '/'); + assert(context.collectionName, 'Collection name should always be available'); + const resolvedCollectionPath = tempRequire.resolve( + join(context.collectionName, 'package.json'), + ); - throw e; + context.collectionName = dirname(resolvedCollectionPath); + } else { + await packageManager.add( + packageIdentifier.toString(), + 'none', + savePackage === 'devDependencies', + false, + true, + { + registry, + }, + ); } } diff --git a/packages/angular/cli/src/commands/cache/utilities.ts b/packages/angular/cli/src/commands/cache/utilities.ts index 84e22314763a..03a6c6ee6f8e 100644 --- a/packages/angular/cli/src/commands/cache/utilities.ts +++ b/packages/angular/cli/src/commands/cache/utilities.ts @@ -7,7 +7,8 @@ */ import { isJsonObject } from '@angular-devkit/core'; -import { resolve } from 'node:path'; +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { dirname, isAbsolute, join, resolve } from 'node:path'; import { Cache, Environment } from '../../../lib/config/workspace-schema'; import { AngularWorkspace } from '../../utilities/config'; @@ -23,13 +24,53 @@ export function updateCacheConfig( return workspace.save(); } +function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): string { + if (isAbsolute(cachePathSetting)) { + return cachePathSetting; + } + + try { + // Find the git directory, walking up from workspaceRoot if necessary + let currentDir = workspaceRoot; + while (true) { + const gitPath = join(currentDir, '.git'); + if (existsSync(gitPath)) { + const stat = statSync(gitPath); + if (stat.isFile()) { + // Could be a git worktree (or submodule) + const content = readFileSync(gitPath, 'utf8'); + const match = /^gitdir:\s*(.+)$/m.exec(content); + if (match) { + const gitdir = resolve(currentDir, match[1].trim()); + const commondirPath = join(gitdir, 'commondir'); + if (existsSync(commondirPath)) { + // It's a git worktree + const commondir = readFileSync(commondirPath, 'utf8').trim(); + const commonGitDir = resolve(gitdir, commondir); + + return resolve(dirname(commonGitDir), cachePathSetting); + } + } + } + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + } catch {} + + return resolve(workspaceRoot, cachePathSetting); +} + export function getCacheConfig(workspace: AngularWorkspace | undefined): Required { if (!workspace) { throw new Error(`Cannot retrieve cache configuration as workspace is not defined.`); } const defaultSettings: Required = { - path: resolve(workspace.basePath, '.angular/cache'), + path: getCacheBasePath(workspace.basePath, '.angular/cache'), environment: Environment.Local, enabled: true, }; @@ -45,14 +86,14 @@ export function getCacheConfig(workspace: AngularWorkspace | undefined): Require } const { - path = defaultSettings.path, + path = '.angular/cache', environment = defaultSettings.environment, enabled = defaultSettings.enabled, // eslint-disable-next-line @typescript-eslint/no-explicit-any } = cacheSettings as Record; return { - path: resolve(workspace.basePath, path), + path: getCacheBasePath(workspace.basePath, path), environment, enabled, }; diff --git a/packages/angular/cli/src/commands/cache/utilities_spec.ts b/packages/angular/cli/src/commands/cache/utilities_spec.ts new file mode 100644 index 000000000000..8179e70309cc --- /dev/null +++ b/packages/angular/cli/src/commands/cache/utilities_spec.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 { workspaces } from '@angular-devkit/core'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { AngularWorkspace } from '../../utilities/config'; +import { getCacheConfig } from './utilities'; + +describe('CLI cache config utilities', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'angular-cli-cache-spec-')); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + function mockWorkspace(basePath: string, cliExtension?: unknown): AngularWorkspace { + return { + basePath, + extensions: cliExtension ? { cli: cliExtension } : {}, + projects: {} as unknown as workspaces.ProjectDefinitionCollection, + filePath: join(basePath, 'angular.json'), + getCli: () => cliExtension, + getProjectCli: () => undefined, + save: () => Promise.resolve(), + } as unknown as AngularWorkspace; + } + + it('should resolve default cache path relative to workspace basePath in a standard repository', async () => { + const workspaceRoot = join(tempDir, 'project'); + await mkdir(join(workspaceRoot, '.git'), { recursive: true }); + + const config = getCacheConfig(mockWorkspace(workspaceRoot)); + + expect(config.path).toBe(resolve(workspaceRoot, '.angular/cache')); + }); + + it('should resolve default cache path relative to main repository root in a git worktree', async () => { + const mainRepoRoot = join(tempDir, 'main-repo'); + const mainGitDir = join(mainRepoRoot, '.git'); + const worktreeRoot = join(tempDir, 'worktree'); + + // Create main repo structure + await mkdir(mainGitDir, { recursive: true }); + + // Create worktree folder and .git file pointing to the main repo's worktree metadata folder + const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1'); + await mkdir(worktreeMetadataDir, { recursive: true }); + await mkdir(worktreeRoot, { recursive: true }); + await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`); + + // Create the commondir file in the worktree metadata folder pointing back to the main .git dir + await writeFile(join(worktreeMetadataDir, 'commondir'), '../..'); + + const config = getCacheConfig(mockWorkspace(worktreeRoot)); + + expect(config.path).toBe(resolve(mainRepoRoot, '.angular/cache')); + }); + + it('should resolve custom relative cache path relative to main repository root in a git worktree', async () => { + const mainRepoRoot = join(tempDir, 'main-repo'); + const mainGitDir = join(mainRepoRoot, '.git'); + const worktreeRoot = join(tempDir, 'worktree'); + + // Create main repo structure + await mkdir(mainGitDir, { recursive: true }); + + // Create worktree folder and .git file pointing to the main repo's worktree metadata folder + const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1'); + await mkdir(worktreeMetadataDir, { recursive: true }); + await mkdir(worktreeRoot, { recursive: true }); + await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`); + + // Create the commondir file in the worktree metadata folder pointing back to the main .git dir + await writeFile(join(worktreeMetadataDir, 'commondir'), '../..'); + + const config = getCacheConfig( + mockWorkspace(worktreeRoot, { cache: { path: 'custom/cache-dir' } }), + ); + + expect(config.path).toBe(resolve(mainRepoRoot, 'custom/cache-dir')); + }); + + it('should resolve cache path relative to workspace basePath in a git submodule', async () => { + const mainRepoRoot = join(tempDir, 'main-repo'); + const submoduleRoot = join(mainRepoRoot, 'submodule'); + + // Create main repo structure and submodule metadata folder + const submoduleGitDir = join(mainRepoRoot, '.git/modules/sub'); + await mkdir(submoduleGitDir, { recursive: true }); + await mkdir(submoduleRoot, { recursive: true }); + + // Create .git file in submodule pointing to the metadata folder + await writeFile(join(submoduleRoot, '.git'), `gitdir: ../.git/modules/sub`); + + // Submodules do NOT have a 'commondir' file. + const config = getCacheConfig(mockWorkspace(submoduleRoot)); + + expect(config.path).toBe(resolve(submoduleRoot, '.angular/cache')); + }); +}); diff --git a/packages/angular/cli/src/commands/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts index 091a9064ca7f..a379022e8f5e 100644 --- a/packages/angular/cli/src/commands/mcp/cli.ts +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -6,14 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { serveStdio } from '@modelcontextprotocol/server/stdio'; import type { Argv } from 'yargs'; import { CommandModule, type CommandModuleImplementation, } from '../../command-builder/command-module'; import { isTTY } from '../../utilities/tty'; -import { EXPERIMENTAL_TOOLS, EXPERIMENTAL_TOOL_GROUPS, createMcpServer } from './mcp-server'; +import { createMcpServer } from './mcp-server'; const INTERACTIVE_MESSAGE = ` To start using the Angular CLI MCP Server, add this configuration to your host: @@ -39,6 +39,12 @@ export default class McpCommandModule extends CommandModule implements CommandMo builder(localYargs: Argv): Argv { return localYargs + .option('root', { + type: 'string', + array: true, + describe: + 'Allowed root directory paths for filesystem access and workspace discovery. Can be specified multiple times.', + }) .option('read-only', { type: 'boolean', default: false, @@ -54,15 +60,12 @@ export default class McpCommandModule extends CommandModule implements CommandMo alias: 'E', array: true, describe: 'Enable an experimental tool.', - choices: [ - ...EXPERIMENTAL_TOOLS.map(({ name }) => name), - ...Object.keys(EXPERIMENTAL_TOOL_GROUPS), - ], hidden: true, }); } async run(options: { + root: string[] | undefined; readOnly: boolean; localOnly: boolean; experimentalTool: string[] | undefined; @@ -73,16 +76,17 @@ export default class McpCommandModule extends CommandModule implements CommandMo return; } - const server = await createMcpServer( - { - workspace: this.context.workspace, - readOnly: options.readOnly, - localOnly: options.localOnly, - experimentalTools: options.experimentalTool, - }, - this.context.logger, + serveStdio(() => + createMcpServer( + { + workspace: this.context.workspace, + readOnly: options.readOnly, + localOnly: options.localOnly, + experimentalTools: options.experimentalTool, + roots: options.root, + }, + this.context.logger, + ), ); - const transport = new StdioServerTransport(); - await server.connect(transport); } } diff --git a/packages/angular/cli/src/commands/mcp/host.ts b/packages/angular/cli/src/commands/mcp/host.ts index 5dda0ade077f..0ec914727e41 100644 --- a/packages/angular/cli/src/commands/mcp/host.ts +++ b/packages/angular/cli/src/commands/mcp/host.ts @@ -282,11 +282,22 @@ export const LocalWorkspaceHost: Host = { }, }; +function resolveRoots(roots: string[]): string[] { + return roots.map((r) => { + try { + return realpathSync(resolve(r)); + } catch { + return resolve(r); + } + }); +} + export function createRootRestrictedHost( baseHost: Host, initialRoots: string[] = [process.cwd()], ): Host { - let roots = initialRoots; + const defaultRoots = resolveRoots(initialRoots); + let roots = defaultRoots; function checkPath(path: string) { const resolvedPath = resolve(path); @@ -332,7 +343,7 @@ export function createRootRestrictedHost( return { ...baseHost, setRoots(newRoots: string[]) { - roots = newRoots; + roots = newRoots.length > 0 ? resolveRoots(newRoots) : defaultRoots; }, stat(path: string) { checkPath(path); diff --git a/packages/angular/cli/src/commands/mcp/host_spec.ts b/packages/angular/cli/src/commands/mcp/host_spec.ts new file mode 100644 index 000000000000..2b2d3d9ea60a --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/host_spec.ts @@ -0,0 +1,60 @@ +/** + * @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 { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { LocalWorkspaceHost, createRootRestrictedHost } from './host'; + +describe('createRootRestrictedHost', () => { + let root1: string; + let root2: string; + let outsideDir: string; + + beforeEach(() => { + root1 = mkdtempSync(join(tmpdir(), 'angular-cli-mcp-root1-')); + root2 = mkdtempSync(join(tmpdir(), 'angular-cli-mcp-root2-')); + outsideDir = mkdtempSync(join(tmpdir(), 'angular-cli-mcp-outside-')); + + writeFileSync(join(root1, 'file1.txt'), 'root 1 content'); + writeFileSync(join(root2, 'file2.txt'), 'root 2 content'); + writeFileSync(join(outsideDir, 'outside.txt'), 'outside content'); + }); + + afterEach(() => { + rmSync(root1, { recursive: true, force: true }); + rmSync(root2, { recursive: true, force: true }); + rmSync(outsideDir, { recursive: true, force: true }); + }); + + it('should allow file access inside any of the configured initial roots', () => { + const host = createRootRestrictedHost(LocalWorkspaceHost, [root1, root2]); + + expect(host.existsSync(join(root1, 'file1.txt'))).toBeTrue(); + expect(host.existsSync(join(root2, 'file2.txt'))).toBeTrue(); + }); + + it('should reject file access outside of the configured roots', () => { + const host = createRootRestrictedHost(LocalWorkspaceHost, [root1, root2]); + + expect(() => host.existsSync(join(outsideDir, 'outside.txt'))).toThrowError( + new RegExp( + `Access denied: path '${join(outsideDir, 'outside.txt')}' is outside allowed roots.`, + ), + ); + }); + + it('should fall back to initial roots when setRoots is called with an empty array', () => { + const host = createRootRestrictedHost(LocalWorkspaceHost, [root1, root2]); + + host.setRoots([]); + + expect(host.existsSync(join(root1, 'file1.txt'))).toBeTrue(); + expect(host.existsSync(join(root2, 'file2.txt'))).toBeTrue(); + }); +}); diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 43320217db6d..e6c7580c6ba9 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -6,9 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { RootsListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; -import { join, normalize } from 'node:path'; +import { McpServer } from '@modelcontextprotocol/server'; +import { join, normalize, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; @@ -41,22 +40,24 @@ const STABLE_TOOLS = [ DOC_SEARCH_TOOL, LIST_PROJECTS_TOOL, ZONELESS_MIGRATION_TOOL, + RUN_TARGET_TOOL, + ...DEVSERVER_TOOLS, ] as const; /** * The set of tools that are available but not enabled by default. * These tools are considered experimental and may have limitations. */ -export const EXPERIMENTAL_TOOLS = [RUN_TARGET_TOOL, ...DEVSERVER_TOOLS] as const; +export const EXPERIMENTAL_TOOLS: readonly AnyMcpToolDeclaration[] = [] as const; /** * Experimental tools that are grouped together under a single name. * * Used for enabling them as a group. */ -export const EXPERIMENTAL_TOOL_GROUPS = { +export const EXPERIMENTAL_TOOL_GROUPS: Record = { 'all': EXPERIMENTAL_TOOLS, - 'devserver': DEVSERVER_TOOLS, + 'devserver': [], }; export async function createMcpServer( @@ -65,6 +66,7 @@ export async function createMcpServer( readOnly?: boolean; localOnly?: boolean; experimentalTools?: string[]; + roots?: string[]; }, logger: { warn(text: string): void }, ): Promise { @@ -120,7 +122,12 @@ for equivalent actions. logger, }); - const restrictedHost = createRootRestrictedHost(LocalWorkspaceHost); + const resolvedRoots = options.roots?.map((r) => resolve(r)); + + const restrictedHost = createRootRestrictedHost( + LocalWorkspaceHost, + resolvedRoots?.length ? resolvedRoots : [process.cwd()], + ); server.server.oninitialized = () => { void (async () => { @@ -132,7 +139,7 @@ for equivalent actions. restrictedHost.setRoots(searchRoots); if (clientCapabilities.roots.listChanged) { - server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { + server.server.setNotificationHandler('notifications/roots/list_changed', async () => { try { const { roots: updatedRoots } = await server.server.listRoots(); const updatedSearchRoots = @@ -162,6 +169,7 @@ for equivalent actions. exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'), devservers: new Map(), host: restrictedHost, + roots: resolvedRoots, }, toolDeclarations, ); @@ -205,7 +213,7 @@ export function assembleToolDeclarations( const tool = experimentalToolsMap.get(toolName); if (tool) { toolDeclarations.push(tool); - } else { + } else if (!stableDeclarations.some((t) => t.name === toolName)) { options.logger.warn(`Unknown experimental tool: ${toolName}`); } } diff --git a/packages/angular/cli/src/commands/mcp/resources/instructions.ts b/packages/angular/cli/src/commands/mcp/resources/instructions.ts index bf85a3d54384..90902d60b389 100644 --- a/packages/angular/cli/src/commands/mcp/resources/instructions.ts +++ b/packages/angular/cli/src/commands/mcp/resources/instructions.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { McpServer } from '@modelcontextprotocol/server'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; diff --git a/packages/angular/cli/src/commands/mcp/testing/test-utils.ts b/packages/angular/cli/src/commands/mcp/testing/test-utils.ts index 1c95c51fe25e..97a11ac863be 100644 --- a/packages/angular/cli/src/commands/mcp/testing/test-utils.ts +++ b/packages/angular/cli/src/commands/mcp/testing/test-utils.ts @@ -7,7 +7,7 @@ */ import { workspaces } from '@angular-devkit/core'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/server'; import { AngularWorkspace } from '../../../utilities/config'; import { type Devserver } from '../devserver'; import { Host } from '../host'; @@ -44,6 +44,9 @@ export interface MockContextOptions { /** Initial set of projects to populate the mock workspace with. */ projects?: Record; + + /** Optional roots to configure in the mock context. */ + roots?: string[]; } /** @@ -75,6 +78,7 @@ export function createMockContext(options: MockContextOptions = {}): { logger: { warn: () => {} }, devservers: new Map(), host, + roots: options.roots, }; return { host, context, projects }; diff --git a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts index dca61eb700b3..0b40c5c0c207 100644 --- a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts +++ b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts @@ -20,6 +20,7 @@ import { createRequire } from 'node:module'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { z } from 'zod'; import { VERSION } from '../../../utilities/version'; +import { isAllowedWorkspacePath } from '../workspace-utils'; import { type McpToolContext, declareTool } from './tool-registry'; const bestPracticesInputSchema = z.object({ @@ -86,13 +87,36 @@ async function getBundledBestPractices(): Promise { * * @param workspacePath The absolute path to the user's `angular.json` file. * @param logger The MCP tool context logger for reporting warnings. + * @param server The MCP server context, used to enforce the client's declared roots. * @returns A promise that resolves to an object containing the guide's content and source, * or `undefined` if the guide could not be resolved. */ async function getVersionSpecificBestPractices( workspacePath: string, logger: McpToolContext['logger'], + server: McpToolContext['server'], ): Promise<{ content: string; source: string } | undefined> { + if (server) { + let isAllowed: boolean; + try { + isAllowed = await isAllowedWorkspacePath(server, workspacePath); + } catch (e) { + logger.warn( + `Failed to verify workspace path '${workspacePath}': ` + + `${e instanceof Error ? e.message : e}. Falling back to the bundled guide.`, + ); + + return undefined; + } + + if (!isAllowed) { + throw new Error( + `Workspace path is outside the allowed MCP roots: ${workspacePath}. ` + + "You can use 'list_projects' to find available workspaces.", + ); + } + } + // 1. Resolve the path to package.json let pkgJsonPath: string; try { @@ -175,7 +199,7 @@ async function getVersionSpecificBestPractices( * @param context The MCP tool context, containing the logger. * @returns An async function that serves as the tool's executor. */ -function createBestPracticesHandler({ logger }: McpToolContext) { +function createBestPracticesHandler({ logger, server }: McpToolContext) { let bundledBestPractices: Promise; return async (input: BestPracticesInput) => { @@ -184,7 +208,11 @@ function createBestPracticesHandler({ logger }: McpToolContext) { // First, try to get the version-specific guide. if (input.workspacePath) { - const versionSpecific = await getVersionSpecificBestPractices(input.workspacePath, logger); + const versionSpecific = await getVersionSpecificBestPractices( + input.workspacePath, + logger, + server, + ); if (versionSpecific) { content = versionSpecific.content; source = versionSpecific.source; diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-start.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-start.ts index 8f5548f14019..72bf5440adcb 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-start.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-start.ts @@ -93,33 +93,33 @@ export const DEVSERVER_START_TOOL: McpToolDeclaration< typeof devserverStartToolInputSchema.shape, typeof devserverStartToolOutputSchema.shape > = declareTool({ - name: 'devserver.start', + name: 'devserver_start', title: 'Start Development Server', description: ` -Starts the Angular development server ("ng serve") as a background process. Follow this up with "devserver.wait_for_build" to wait until +Starts the Angular development server ("ng serve") as a background process. Follow this up with "devserver_wait_for_build" to wait until the first build completes. * **Starting the Server:** Use this tool to begin serving the application. The tool will return immediately while the server runs in the background. -* **Get Initial Build Logs:** Once a dev server has started, use the "devserver.wait_for_build" tool to ensure it's alive. If there are any - build errors, "devserver.wait_for_build" would provide them back and you can give them to the user or rely on them to propose a fix. -* **Get Updated Build Logs:** Important: as long as a devserver is alive (i.e. "devserver.stop" wasn't called), after every time you - make a change to the workspace, re-run "devserver.wait_for_build" to see whether the change was successfully built and wait for the +* **Get Initial Build Logs:** Once a dev server has started, use the "devserver_wait_for_build" tool to ensure it's alive. If there are any + build errors, "devserver_wait_for_build" would provide them back and you can give them to the user or rely on them to propose a fix. +* **Get Updated Build Logs:** Important: as long as a devserver is alive (i.e. "devserver_stop" wasn't called), after every time you + make a change to the workspace, re-run "devserver_wait_for_build" to see whether the change was successfully built and wait for the devserver to be updated. * This tool manages development servers by itself. It maintains at most a single dev server instance for each project in the monorepo. * This is an asynchronous operation. Subsequent commands can be ran while the server is active. -* Use 'devserver.stop' to gracefully shut down the server and access the full log output. +* Use 'devserver_stop' to gracefully shut down the server and access the full log output. * **Keeping the Server Alive**: It is often better to keep the server alive between tool calls if you expect the user to request more changes or run more tests, as it saves time on restarts and maintains the file watcher state. You must still call - 'devserver.wait_for_build' after every change to see whether the change was successfully built and be sure that that app was updated. + 'devserver_wait_for_build' after every change to see whether the change was successfully built and be sure that the app was updated. * **Consistent Ports**: If making multiple calls, it is recommended to reuse the port you got from the first call for subsequent ones. `, - isReadOnly: true, + isReadOnly: false, isLocalOnly: true, inputSchema: devserverStartToolInputSchema.shape, outputSchema: devserverStartToolOutputSchema.shape, diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-stop.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-stop.ts index 1c90bd9ecd98..32b7d90269d2 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-stop.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-stop.ts @@ -54,11 +54,11 @@ export const DEVSERVER_STOP_TOOL: McpToolDeclaration< typeof devserverStopToolInputSchema.shape, typeof devserverStopToolOutputSchema.shape > = declareTool({ - name: 'devserver.stop', + name: 'devserver_stop', title: 'Stop Development Server', description: ` -Stops a running Angular development server ("ng serve") that was started with the "devserver.start" tool. +Stops a running Angular development server ("ng serve") that was started with the "devserver_start" tool. * **Stopping the Server:** Use this tool to terminate a running development server and retrieve the logs. @@ -69,7 +69,7 @@ Stops a running Angular development server ("ng serve") that was started with th time after this is called. However note that this is not a blocker for starting a new devserver. `, - isReadOnly: true, + isReadOnly: false, isLocalOnly: true, inputSchema: devserverStopToolInputSchema.shape, outputSchema: devserverStopToolOutputSchema.shape, diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-wait-for-build.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-wait-for-build.ts index c64a7098417c..aa0c76bb49c9 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-wait-for-build.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/devserver-wait-for-build.ts @@ -96,22 +96,22 @@ export const DEVSERVER_WAIT_FOR_BUILD_TOOL: McpToolDeclaration< typeof devserverWaitForBuildToolInputSchema.shape, typeof devserverWaitForBuildToolOutputSchema.shape > = declareTool({ - name: 'devserver.wait_for_build', + name: 'devserver_wait_for_build', title: 'Wait for Devserver Build', description: ` -Waits for a dev server that was started with the "devserver.start" tool to complete its build, then reports the build logs from its most +Waits for a dev server that was started with the "devserver_start" tool to complete its build, then reports the build logs from its most recent build. -* **Waiting for a build:** As long as a devserver is alive ("devserver.start" was called for this project and "devserver.stop" wasn't +* **Waiting for a build:** As long as a devserver is alive ("devserver_start" was called for this project and "devserver_stop" wasn't called yet), then if you're making a file change and want to ensure it was successfully built, call this tool instead of any other build tool or command. When it retuns you'll get build logs back **and** you'll know the user's devserver is up-to-date with the latest changes. -* This tool expects that a dev server was launched on the same project with the "devserver.start" tool, otherwise the tool will fail. +* This tool expects that a dev server was launched on the same project with the "devserver_start" tool, otherwise the tool will fail. * This tool will block until the build is complete or the timeout is reached. If you expect a long build process, consider increasing the - timeout. Timeouts on initial run (right after "devserver.start" calls) or after a big change are not necessarily indicative of an error. + timeout. Timeouts on initial run (right after "devserver_start" calls) or after a big change are not necessarily indicative of an error. * If you encountered a timeout and it might be reasonable, just call this tool again. * If the dev server is not building, it will return quickly, with the logs from the last build. diff --git a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts index 145d819f278b..310dd4809067 100644 --- a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { LegacySearchMethodProps, SearchResponse } from 'algoliasearch'; import { createDecipheriv } from 'node:crypto'; import { Readable } from 'node:stream'; import { z } from 'zod'; @@ -32,7 +31,7 @@ const MIN_SUPPORTED_DOCS_VERSION = 17; * condition where a newly released CLI might default to searching for a documentation index that * doesn't exist yet. */ -const LATEST_KNOWN_DOCS_VERSION = 20; +const LATEST_KNOWN_DOCS_VERSION = 22; const docSearchInputSchema = z.object({ query: z @@ -99,41 +98,86 @@ Searches the official Angular documentation (angular.dev) to answer questions ab }); function createDocSearchHandler({ logger }: McpToolContext) { - let client: import('algoliasearch').SearchClient | undefined; + let apiKey: string | undefined; - return async ({ query, includeTopContent, version }: DocSearchInput) => { - if (!client) { + async function performSearch(query: string, version: number) { + if (!apiKey) { const dcip = createDecipheriv( 'aes-256-gcm', (k1 + ALGOLIA_APP_ID).padEnd(32, '^'), iv, ).setAuthTag(Buffer.from(at, 'base64')); - const { searchClient } = await import('algoliasearch'); - client = searchClient( - ALGOLIA_APP_ID, - dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'), + apiKey = dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'); + } + + const url = `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/angular_v${version}/query`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Algolia-Application-Id': ALGOLIA_APP_ID, + 'X-Algolia-API-Key': apiKey, + }, + body: JSON.stringify({ + query, + attributesToRetrieve: [ + 'hierarchy.lvl0', + 'hierarchy.lvl1', + 'hierarchy.lvl2', + 'hierarchy.lvl3', + 'hierarchy.lvl4', + 'hierarchy.lvl5', + 'hierarchy.lvl6', + 'content', + 'type', + 'url', + ], + hitsPerPage: 10, + }), + signal: AbortSignal.timeout(5000), // Timeout after 5 seconds + }); + + if (!response.ok) { + throw new Error( + `Search request failed with status ${response.status} (${response.statusText})`, ); } + const data = (await response.json()) as { hits: Record[] }; + + return data.hits; + } + + return async ({ query, includeTopContent, version }: DocSearchInput) => { let finalSearchedVersion = Math.max( version ?? LATEST_KNOWN_DOCS_VERSION, MIN_SUPPORTED_DOCS_VERSION, ); - let searchResults; + + let allHits: Record[] | undefined; try { - searchResults = await client.search(createSearchArguments(query, finalSearchedVersion)); - } catch {} + allHits = await performSearch(query, finalSearchedVersion); + } catch (error) { + logger.warn(`Error searching Angular v${finalSearchedVersion} documentation: ${error}`); + } // If the initial search for a newer-than-stable version returns no results, it may be because // the index for that version doesn't exist yet. In this case, fall back to the latest known // stable version. - if (!searchResults && finalSearchedVersion > LATEST_KNOWN_DOCS_VERSION) { + if ((!allHits || allHits.length === 0) && finalSearchedVersion > LATEST_KNOWN_DOCS_VERSION) { + logger.warn( + `Documentation index for v${finalSearchedVersion} not found or empty. Falling back to v${LATEST_KNOWN_DOCS_VERSION}.`, + ); finalSearchedVersion = LATEST_KNOWN_DOCS_VERSION; - searchResults = await client.search(createSearchArguments(query, finalSearchedVersion)); + try { + allHits = await performSearch(query, finalSearchedVersion); + } catch (error) { + logger.warn( + `Error searching fallback Angular v${finalSearchedVersion} documentation: ${error}`, + ); + } } - const allHits = searchResults?.results.flatMap((result) => (result as SearchResponse).hits); - if (!allHits?.length) { return { content: [ @@ -282,38 +326,3 @@ function formatHitToParts(hit: Record): { title: string; breadc return { title, breadcrumb }; } - -/** - * Creates the search arguments for an Algolia search. - * - * The arguments are based on the search implementation in `adev`. - * - * @param query The search query string. - * @returns The search arguments for the Algolia client. - */ -function createSearchArguments(query: string, version: number): LegacySearchMethodProps { - // Search arguments are based on adev's search service: - // https://github.com/angular/angular/blob/4b614fbb3263d344dbb1b18fff24cb09c5a7582d/adev/shared-docs/services/search.service.ts#L58 - return [ - { - indexName: `angular_v${version}`, - params: { - query, - attributesToRetrieve: [ - 'hierarchy.lvl0', - 'hierarchy.lvl1', - 'hierarchy.lvl2', - 'hierarchy.lvl3', - 'hierarchy.lvl4', - 'hierarchy.lvl5', - 'hierarchy.lvl6', - 'content', - 'type', - 'url', - ], - hitsPerPage: 10, - }, - type: 'default', - }, - ]; -} diff --git a/packages/angular/cli/src/commands/mcp/tools/doc-search_spec.ts b/packages/angular/cli/src/commands/mcp/tools/doc-search_spec.ts new file mode 100644 index 000000000000..b07bec0e9cf6 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search_spec.ts @@ -0,0 +1,107 @@ +/** + * @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 { createMockContext } from '../testing/test-utils'; +import { DOC_SEARCH_TOOL } from './doc-search'; + +describe('Doc Search Tool', () => { + let mockContext: ReturnType['context']; + let fetchSpy: jasmine.Spy; + + beforeEach(() => { + const { context } = createMockContext(); + mockContext = context; + + fetchSpy = spyOn(globalThis, 'fetch'); + }); + + it('should query the correct Algolia endpoint with headers and payload', async () => { + fetchSpy.and.resolveTo( + new Response( + JSON.stringify({ + hits: [ + { + hierarchy: { + lvl0: 'Docs', + lvl1: 'Standalone Components', + }, + url: 'https://angular.dev/guide/standalone-components', + }, + ], + }), + { status: 200, statusText: 'OK' }, + ), + ); + + const handler = DOC_SEARCH_TOOL.factory(mockContext); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await (handler as any)({ + query: 'standalone', + version: 22, + }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [calledUrl, calledInit] = fetchSpy.calls.first().args; + expect(calledUrl).toBe('https://L1XWT2UJ7F-dsn.algolia.net/1/indexes/angular_v22/query'); + expect(calledInit.method).toBe('POST'); + expect(calledInit.headers['X-Algolia-Application-Id']).toBe('L1XWT2UJ7F'); + expect(calledInit.headers['X-Algolia-API-Key']).toBeDefined(); + + const body = JSON.parse(calledInit.body); + expect(body.query).toBe('standalone'); + + expect(result.structuredContent.searchedVersion).toBe(22); + expect(result.structuredContent.results.length).toBe(1); + expect(result.structuredContent.results[0].title).toBe('Standalone Components'); + }); + + it('should fallback to latest known version if initial search returns no hits', async () => { + // First call returns empty hits + fetchSpy.and.returnValues( + Promise.resolve( + new Response(JSON.stringify({ hits: [] }), { status: 200, statusText: 'OK' }), + ), + // Second fallback call returns a hit + Promise.resolve( + new Response( + JSON.stringify({ + hits: [ + { + hierarchy: { + lvl0: 'Docs', + lvl1: 'Fallback Guide', + }, + url: 'https://angular.dev/guide/fallback', + }, + ], + }), + { status: 200, statusText: 'OK' }, + ), + ), + ); + + const handler = DOC_SEARCH_TOOL.factory(mockContext); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await (handler as any)({ + query: 'some-query', + version: 24, + }); + + expect(fetchSpy).toHaveBeenCalledTimes(2); + expect(fetchSpy.calls.first().args[0]).toBe( + 'https://L1XWT2UJ7F-dsn.algolia.net/1/indexes/angular_v24/query', + ); + expect(fetchSpy.calls.mostRecent().args[0]).toBe( + 'https://L1XWT2UJ7F-dsn.algolia.net/1/indexes/angular_v22/query', + ); + + expect(result.structuredContent.searchedVersion).toBe(22); + expect(result.structuredContent.results.length).toBe(1); + expect(result.structuredContent.results[0].title).toBe('Fallback Guide'); + }); +}); diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts index ebbc254d4d69..58f5842203a5 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { ServerContext } from '@modelcontextprotocol/server'; import type { SourceFile } from 'typescript'; import type { Host } from '../../host'; import { analyzeForUnsupportedZoneUses } from './analyze-for-unsupported-zone-uses'; @@ -22,7 +21,7 @@ const supportedStrategies: ReadonlySet = new Set(['OnPush', 'Default', ' export async function migrateSingleFile( sourceFile: SourceFile, host: Host, - extras: RequestHandlerExtra, + extras: ServerContext, ): Promise { const testBedSpecifier = await getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed'); const isTestFile = sourceFile.fileName.endsWith('.spec.ts') || !!testBedSpecifier; diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts index 4f6337d8e434..8609977fc417 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts @@ -6,16 +6,17 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { ServerContext } from '@modelcontextprotocol/server'; import ts from 'typescript'; import { createMockHost } from '../../testing/test-utils'; import { migrateSingleFile } from './migrate-single-file'; const fakeExtras = { - sendDebugMessage: jasmine.createSpy(), - sendNotification: jasmine.createSpy(), -} as unknown as RequestHandlerExtra; + mcpReq: { + log: jasmine.createSpy(), + notify: jasmine.createSpy(), + }, +} as unknown as ServerContext; describe('migrateSingleFile', () => { const mockHost = createMockHost(); diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts index b2f5436d497a..8e0c5fe9ca11 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts @@ -6,18 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { ServerContext } from '@modelcontextprotocol/server'; -export function sendDebugMessage( - message: string, - { sendNotification }: RequestHandlerExtra, -): void { - void sendNotification({ - method: 'notifications/message', - params: { - level: 'debug', - data: message, - }, - }); +export function sendDebugMessage(message: string, ctx: ServerContext): void { + void ctx.mcpReq.log('debug', message); } diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts index 25c12aa83378..36fc7f91b5ae 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { ServerContext } from '@modelcontextprotocol/server'; import { join } from 'node:path'; import type { SourceFile } from 'typescript'; import { z } from 'zod'; @@ -57,7 +56,7 @@ change detection (a prerequisite for zoneless applications). export async function registerZonelessMigrationTool( fileOrDirPath: string, host: Host, - extras: RequestHandlerExtra, + extras: ServerContext, ) { let filesWithComponents, componentTestFiles, zoneFiles, categorizationErrors; try { @@ -114,7 +113,7 @@ export async function registerZonelessMigrationTool( async function discoverAndCategorizeFiles( fileOrDirPath: string, host: Host, - extras: RequestHandlerExtra, + extras: ServerContext, ) { const filePaths: string[] = []; const componentTestFiles = new Set(); @@ -175,7 +174,7 @@ async function discoverAndCategorizeFiles( async function categorizeFile( sourceFile: SourceFile, host: Host, - extras: RequestHandlerExtra, + extras: ServerContext, categorizedFiles: { filesWithComponents: Set; componentTestFiles: Set; @@ -216,11 +215,11 @@ async function categorizeFile( } async function rankComponentFilesForMigration( - { sendRequest }: RequestHandlerExtra, + ctx: ServerContext, componentFiles: SourceFile[], ): Promise { try { - const response = await sendRequest( + const response = await ctx.mcpReq.send( { method: 'sampling/createMessage', params: { diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index 57efa39c4aa2..5b9d15808b44 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -386,11 +386,9 @@ async function getProjectStyleLanguage( fullSourceRoot: string, ): Promise { const projectSchematics = project.extensions.schematics as - | Record> - | undefined; + Record> | undefined; const workspaceSchematics = workspace.extensions.schematics as - | Record> - | undefined; + Record> | undefined; // 1. Check for a project-specific schematic setting. let style = projectSchematics?.['@schematics/angular:component']?.['style']; @@ -566,7 +564,7 @@ function deduplicateSearchRoots(roots: string[]): string[] { return deduplicated; } -async function createListProjectsHandler({ server }: McpToolContext) { +async function createListProjectsHandler({ server, roots: configuredRoots }: McpToolContext) { return async () => { const workspaces: WorkspaceData[] = []; const parsingErrors: ParsingError[] = []; @@ -574,14 +572,16 @@ async function createListProjectsHandler({ server }: McpToolContext) { const seenPaths = new Set(); const versionCache = new Map(); - let searchRoots: string[]; + let searchRoots: string[] | undefined; const clientCapabilities = server.server.getClientCapabilities(); if (clientCapabilities?.roots) { const { roots } = await server.server.listRoots(); - searchRoots = roots?.map((r) => normalize(fileURLToPath(r.uri))) ?? []; - } else { - // Fallback to the current working directory if client does not support roots - searchRoots = [process.cwd()]; + searchRoots = roots?.map((r) => normalize(fileURLToPath(r.uri))); + } + + if (!searchRoots || searchRoots.length === 0) { + searchRoots = + configuredRoots && configuredRoots.length > 0 ? configuredRoots : [process.cwd()]; } searchRoots = deduplicateSearchRoots(searchRoots); diff --git a/packages/angular/cli/src/commands/mcp/tools/projects_spec.ts b/packages/angular/cli/src/commands/mcp/tools/projects_spec.ts index b45b1bbcb189..9c5e2d7d08f8 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects_spec.ts @@ -89,4 +89,43 @@ describe('List Projects Tool', () => { expect(projects[0].targets).toEqual(['build', 'test', 'lint', 'e2e']); expect(projects[0].unitTestFramework).toBe('vitest'); }); + + it('should use configured roots when client roots capability is absent', async () => { + mockContext.server = { + server: { + getClientCapabilities: jasmine.createSpy('getClientCapabilities').and.returnValue({}), + }, + } as unknown as NonNullable[0]['server']>; + mockContext.roots = [allowedRoot]; + + const handler = await LIST_PROJECTS_TOOL.factory(mockContext); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await (handler as any)({}); + + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent.workspaces.length).toBe(1); + expect(result.structuredContent.workspaces[0].projects[0].name).toBe('my-app'); + }); + + it('should fall back to configured roots when client supports roots but returns an empty list', async () => { + mockContext.server = { + server: { + getClientCapabilities: jasmine.createSpy('getClientCapabilities').and.returnValue({ + roots: { listChanged: false }, + }), + listRoots: jasmine.createSpy('listRoots').and.resolveTo({ + roots: [], + }), + }, + } as unknown as NonNullable[0]['server']>; + mockContext.roots = [allowedRoot]; + + const handler = await LIST_PROJECTS_TOOL.factory(mockContext); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await (handler as any)({}); + + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent.workspaces.length).toBe(1); + expect(result.structuredContent.workspaces[0].projects[0].name).toBe('my-app'); + }); }); diff --git a/packages/angular/cli/src/commands/mcp/tools/run-target/build-target-strategy.ts b/packages/angular/cli/src/commands/mcp/tools/run-target/build-target-strategy.ts index 1fbd8f83e47a..2e280927f466 100644 --- a/packages/angular/cli/src/commands/mcp/tools/run-target/build-target-strategy.ts +++ b/packages/angular/cli/src/commands/mcp/tools/run-target/build-target-strategy.ts @@ -29,7 +29,7 @@ export class BuildTargetStrategy implements TargetStrategy { ): Promise { const args = ['build', input.projectName]; if (input.configuration) { - args.push('-c', input.configuration); + args.push(`--configuration=${input.configuration}`); } args.push(...serializeOptions(input.options)); diff --git a/packages/angular/cli/src/commands/mcp/tools/run-target/generic-target-strategy.ts b/packages/angular/cli/src/commands/mcp/tools/run-target/generic-target-strategy.ts index f1d66b5597e7..365253131248 100644 --- a/packages/angular/cli/src/commands/mcp/tools/run-target/generic-target-strategy.ts +++ b/packages/angular/cli/src/commands/mcp/tools/run-target/generic-target-strategy.ts @@ -34,7 +34,7 @@ export class GenericTargetStrategy implements TargetStrategy { if (input.targetName === 'serve' || input.options?.['watch'] === true) { throw new Error( `Watch mode execution (serve target or watch option) is not yet supported by 'run_target'. ` + - `Please use the legacy 'devserver.start' / 'devserver.wait_for_build' tools instead.`, + `Please use the legacy 'devserver_start' / 'devserver_wait_for_build' tools instead.`, ); } @@ -46,7 +46,7 @@ export class GenericTargetStrategy implements TargetStrategy { } if (input.configuration) { - args.push('-c', input.configuration); + args.push(`--configuration=${input.configuration}`); } let options = input.options; diff --git a/packages/angular/cli/src/commands/mcp/tools/run-target/run-target.ts b/packages/angular/cli/src/commands/mcp/tools/run-target/run-target.ts index 8b6f62d8fcba..71dee769c836 100644 --- a/packages/angular/cli/src/commands/mcp/tools/run-target/run-target.ts +++ b/packages/angular/cli/src/commands/mcp/tools/run-target/run-target.ts @@ -66,7 +66,7 @@ This is the single, unified interface for executing all project tasks natively. and disables watch mode to guarantee clean execution. * Output Paths: For official builders, successful builds return the build directory in 'outputPath' under the extensions metadata. * Watch mode (serve target or watch options) is NOT yet supported in this version of run_target. - You MUST use the legacy 'devserver.*' tools for background server lifecycles. + You MUST use the legacy 'devserver_*' tools for background server lifecycles. `, isReadOnly: false, isLocalOnly: true, diff --git a/packages/angular/cli/src/commands/mcp/tools/run-target/run-target_spec.ts b/packages/angular/cli/src/commands/mcp/tools/run-target/run-target_spec.ts index 67b1fffef27e..91ccbac4584a 100644 --- a/packages/angular/cli/src/commands/mcp/tools/run-target/run-target_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/run-target/run-target_spec.ts @@ -42,7 +42,7 @@ describe('Run Target Tool', () => { mockContext.workspace.extensions['defaultProject'] = 'my-app'; await runTarget({ target: 'build', configuration: 'production' }, mockContext); expect(mockHost.executeNgCommand).toHaveBeenCalledWith( - ['build', 'my-app', '-c', 'production'], + ['build', 'my-app', '--configuration=production'], { cwd: '/test', }, @@ -53,7 +53,7 @@ describe('Run Target Tool', () => { mockContext.workspace.extensions['defaultProject'] = 'my-app'; await runTarget({ target: 'storybook', configuration: 'docs' }, mockContext); expect(mockHost.executeNgCommand).toHaveBeenCalledWith( - ['run', 'my-app:storybook', '-c', 'docs'], + ['run', 'my-app:storybook', '--configuration=docs'], { cwd: '/test' }, ); }); diff --git a/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy.ts b/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy.ts index 77ca5797e71c..1023cdd7956c 100644 --- a/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy.ts +++ b/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy.ts @@ -28,7 +28,7 @@ export class UnitTestTargetStrategy implements TargetStrategy { ): Promise { const args = ['test', input.projectName]; if (input.configuration) { - args.push('-c', input.configuration); + args.push(`--configuration=${input.configuration}`); } const builder = input.targetDefinition?.builder; diff --git a/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy_spec.ts b/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy_spec.ts index f1467f0002c9..4e63eed148a0 100644 --- a/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/run-target/unit-test-strategy_spec.ts @@ -65,7 +65,15 @@ describe('UnitTestTargetStrategy', () => { ); expect(mockHost.executeNgCommand).toHaveBeenCalledWith( - ['test', 'my-app', '-c', 'ci', '--browsers', 'ChromeHeadless', '--watch', 'false'], + [ + 'test', + 'my-app', + '--configuration=ci', + '--browsers', + 'ChromeHeadless', + '--watch', + 'false', + ], { cwd: '/test' }, ); }); diff --git a/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts b/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts index 5af40a4e98b5..2784798d6de4 100644 --- a/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts +++ b/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts @@ -6,15 +6,17 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types'; -import type { ZodRawShape } from 'zod'; +import type { + McpServer, + ServerContext, + ToolAnnotations, + ToolCallback, +} from '@modelcontextprotocol/server'; +import { type ZodRawShape, z } from 'zod'; import type { AngularWorkspace } from '../../../utilities/config'; import type { Devserver } from '../devserver'; import type { Host } from '../host'; -type ToolConfig = Parameters[1]; - export interface McpToolContext { server: McpServer; workspace?: AngularWorkspace; @@ -22,11 +24,17 @@ export interface McpToolContext { exampleDatabasePath?: string; devservers: Map; host: Host; + roots?: string[]; } +export type McpToolCallback = ( + args: z.infer>, + ctx: ServerContext, +) => ReturnType; + export type McpToolFactory = ( context: McpToolContext, -) => ToolCallback | Promise>; +) => McpToolCallback | Promise>; export interface McpToolDeclaration { name: string; @@ -75,6 +83,14 @@ export async function registerTools( config.annotations.openWorldHint = !isLocalOnly; } - server.registerTool(name, config, handler); + server.registerTool( + name, + { + ...config, + inputSchema: config.inputSchema ? z.object(config.inputSchema) : undefined, + outputSchema: config.outputSchema ? z.object(config.outputSchema) : undefined, + }, + handler, + ); } } diff --git a/packages/angular/cli/src/commands/mcp/workspace-utils.ts b/packages/angular/cli/src/commands/mcp/workspace-utils.ts index 6cc245ff1dbc..94c4c55655c0 100644 --- a/packages/angular/cli/src/commands/mcp/workspace-utils.ts +++ b/packages/angular/cli/src/commands/mcp/workspace-utils.ts @@ -110,7 +110,7 @@ async function getAllowedWorkspaceRoots(server: McpToolContext['server']): Promi .filter((root): root is string => root !== null); } -async function isAllowedWorkspacePath( +export async function isAllowedWorkspacePath( server: McpToolContext['server'], workspacePath: string, ): Promise { diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts index 62416b1c1ee7..005df998b501 100644 --- a/packages/angular/cli/src/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -24,6 +24,13 @@ import type { InstalledPackage, PackageManager, PackageManifest } from '../../pa import { colors } from '../../utilities/color'; import { disableVersionCheck } from '../../utilities/environment-options'; import { assertIsError } from '../../utilities/error'; +import { + UpdatePlan, + applyUpdatePlan, + findPackageJson, + printUpdateUsageMessage, + resolveUserUpdatePlan, +} from './update-resolver'; import { checkCLIVersion, coerceVersionNumber, @@ -32,12 +39,7 @@ import { } from './utilities/cli-version'; import { ANGULAR_PACKAGES_REGEXP } from './utilities/constants'; import { checkCleanGit } from './utilities/git'; -import { - commitChanges, - executeMigration, - executeMigrations, - executeSchematic, -} from './utilities/migration'; +import { commitChanges, executeMigration, executeMigrations } from './utilities/migration'; interface UpdateCommandArgs { packages?: string[]; @@ -54,8 +56,6 @@ interface UpdateCommandArgs { class CommandError extends Error {} -const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json'); - export default class UpdateCommandModule extends CommandModule { override scope = CommandScope.In; protected override shouldReportAnalytics = false; @@ -244,23 +244,34 @@ export default class UpdateCommandModule extends CommandModule/package.json` after installation, + * we ensure that any migration collections defined by the package are discovered and queued. + */ +export async function resolveFallbackMigrations( + workspaceRoot: string, + plan: UpdatePlan, +): Promise<{ package: string; collection: string; from: string; to: string }[]> { + const migrations = [...plan.migrationsToRun]; + const existingMigrationPackages = new Set(migrations.map((m) => m.package)); + + for (const [packageName, targetVersion] of plan.packagesToUpdate) { + if (existingMigrationPackages.has(packageName)) { + continue; + } - return packageJsonPath; - } catch { - return undefined; + const packageJsonPath = findPackageJson(workspaceRoot, packageName); + if (packageJsonPath) { + try { + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); + const ngUpdate = packageJson?.['ng-update']; + if (ngUpdate && typeof ngUpdate === 'object' && typeof ngUpdate.migrations === 'string') { + const installedVersion = plan.packageInfoMap.get(packageName)?.installed.version; + if (installedVersion) { + migrations.push({ + package: packageName, + collection: ngUpdate.migrations, + from: installedVersion, + to: targetVersion, + }); + } + } + } catch { + // Ignore read/parse errors for optional fallback + } + } } + + return migrations; } diff --git a/packages/angular/cli/src/commands/update/cli_spec.ts b/packages/angular/cli/src/commands/update/cli_spec.ts new file mode 100644 index 000000000000..bae00d3dba24 --- /dev/null +++ b/packages/angular/cli/src/commands/update/cli_spec.ts @@ -0,0 +1,133 @@ +/** + * @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 { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import path from 'path'; +import { resolveFallbackMigrations } from './cli'; +import type { PackageVersionInfo, UpdatePlan } from './update-resolver'; + +describe('resolveFallbackMigrations', () => { + let tempRoot: string; + let pkgDir: string; + beforeEach(async () => { + const baseTmpDir = process.env['TEST_TMPDIR']; + assert(baseTmpDir, 'TEST_TMPDIR is not set'); + tempRoot = await mkdtemp(path.join(baseTmpDir, 'angular-cli-update-cli-test-')); + pkgDir = path.join(tempRoot, 'node_modules/@company/library-name'); + await mkdir(pkgDir, { recursive: true }); + }); + + afterEach(async () => { + await rm(tempRoot, { recursive: true, force: true }); + }); + + it('discovers migrations from installed package.json when omitted from plan.migrationsToRun', async () => { + await writeFile( + path.join(pkgDir, 'package.json'), + JSON.stringify({ + name: '@company/library-name', + version: '21.2.0-next.1', + 'ng-update': { + migrations: './schematics/migration.json', + }, + }), + 'utf8', + ); + + const plan: UpdatePlan = { + packagesToUpdate: new Map([['@company/library-name', '21.2.0-next.1']]), + migrationsToRun: [], + packageInfoMap: new Map([ + [ + '@company/library-name', + { + name: '@company/library-name', + npmPackageJson: { + name: '@company/library-name', + versions: ['21.1.0', '21.2.0-next.1'], + 'dist-tags': {}, + }, + installed: { + version: '21.1.0' as unknown as PackageVersionInfo['version'], + packageJson: { name: '@company/library-name', version: '21.1.0' }, + updateMetadata: { packageGroup: {}, requirements: {} }, + }, + packageJsonRange: '^21.1.0', + }, + ], + ]), + registryClient: undefined as unknown as UpdatePlan['registryClient'], + }; + + const migrations = await resolveFallbackMigrations(tempRoot, plan); + + expect(migrations).toEqual([ + { + package: '@company/library-name', + collection: './schematics/migration.json', + from: '21.1.0', + to: '21.2.0-next.1', + }, + ]); + }); + + it('does not duplicate migration if package is already in plan.migrationsToRun', async () => { + await writeFile( + path.join(pkgDir, 'package.json'), + JSON.stringify({ + name: '@company/library-name', + version: '21.2.0-next.1', + 'ng-update': { + migrations: './schematics/migration.json', + }, + }), + 'utf8', + ); + + const plan: UpdatePlan = { + packagesToUpdate: new Map([['@company/library-name', '21.2.0-next.1']]), + migrationsToRun: [ + { + package: '@company/library-name', + collection: './schematics/migration.json', + from: '21.1.0', + to: '21.2.0-next.1', + }, + ], + packageInfoMap: new Map(), + registryClient: undefined as unknown as UpdatePlan['registryClient'], + }; + + const migrations = await resolveFallbackMigrations(tempRoot, plan); + + expect(migrations).toHaveSize(1); + }); + + it('returns unchanged migrations when package has no ng-update field on disk', async () => { + await writeFile( + path.join(pkgDir, 'package.json'), + JSON.stringify({ + name: '@company/library-name', + version: '21.2.0-next.1', + }), + 'utf8', + ); + + const plan: UpdatePlan = { + packagesToUpdate: new Map([['@company/library-name', '21.2.0-next.1']]), + migrationsToRun: [], + packageInfoMap: new Map(), + registryClient: undefined as unknown as UpdatePlan['registryClient'], + }; + + const migrations = await resolveFallbackMigrations(tempRoot, plan); + + expect(migrations).toHaveSize(0); + }); +}); diff --git a/packages/angular/cli/src/commands/update/schematic/collection.json b/packages/angular/cli/src/commands/update/schematic/collection.json deleted file mode 100644 index ee7197918bd6..000000000000 --- a/packages/angular/cli/src/commands/update/schematic/collection.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schematics": { - "update": { - "factory": "./index", - "schema": "./schema.json", - "description": "Update one or multiple packages to versions, updating peer dependencies along the way." - } - } -} diff --git a/packages/angular/cli/src/commands/update/schematic/index.ts b/packages/angular/cli/src/commands/update/schematic/index.ts deleted file mode 100644 index 23053c031aee..000000000000 --- a/packages/angular/cli/src/commands/update/schematic/index.ts +++ /dev/null @@ -1,1128 +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 { logging } from '@angular-devkit/core'; -import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; -import { existsSync, readFileSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import * as path from 'node:path'; -import * as npa from 'npm-package-arg'; -import * as semver from 'semver'; -import { - NpmRepositoryPackageJson, - PackageManifest, - getNpmPackageJson, -} from '../../../utilities/package-metadata'; -import { Schema as UpdateSchema } from './schema'; - -type VersionRange = string & { __VERSION_RANGE: void }; -type PeerVersionTransform = string | ((range: string) => string); - -// Angular guarantees that a major is compatible with its following major (so packages that depend -// on Angular 5 are also compatible with Angular 6). This is, in code, represented by verifying -// that all other packages that have a peer dependency of `"@angular/core": "^5.0.0"` actually -// supports 6.0, by adding that compatibility to the range, so it is `^5.0.0 || ^6.0.0`. -// We export it to allow for testing. -export function angularMajorCompatGuarantee(range: string) { - let newRange = semver.validRange(range); - if (!newRange) { - return range; - } - let major = 1; - while (!semver.gtr(major + '.0.0', newRange)) { - major++; - if (major >= 99) { - // Use original range if it supports a major this high - // Range is most likely unbounded (e.g., >=5.0.0) - return newRange; - } - } - - // Add the major version as compatible with the angular compatible, with all minors. This is - // already one major above the greatest supported, because we increment `major` before checking. - // We add minors like this because a minor beta is still compatible with a minor non-beta. - newRange = range; - for (let minor = 0; minor < 20; minor++) { - newRange += ` || ^${major}.${minor}.0-alpha.0 `; - } - - return semver.validRange(newRange) || range; -} - -// This is a map of packageGroupName to range extending function. If it isn't found, the range is -// kept the same. -const knownPeerCompatibleList: { [name: string]: PeerVersionTransform } = { - '@angular/core': angularMajorCompatGuarantee, -}; - -interface PackageVersionInfo { - version: VersionRange; - packageJson: PackageManifest; - updateMetadata: UpdateMetadata; -} - -interface PackageInfo { - name: string; - npmPackageJson: NpmRepositoryPackageJson; - installed: PackageVersionInfo; - target?: PackageVersionInfo; - packageJsonRange: string; -} - -interface UpdateMetadata { - packageGroupName?: string; - packageGroup: { [packageName: string]: string }; - requirements: { [packageName: string]: string }; - migrations?: string; -} - -function _updatePeerVersion(infoMap: Map, name: string, range: string) { - // Resolve packageGroupName. - const maybePackageInfo = infoMap.get(name); - if (!maybePackageInfo) { - return range; - } - if (maybePackageInfo.target) { - name = maybePackageInfo.target.updateMetadata.packageGroupName || name; - } else { - name = maybePackageInfo.installed.updateMetadata.packageGroupName || name; - } - - const maybeTransform = knownPeerCompatibleList[name]; - if (maybeTransform) { - if (typeof maybeTransform == 'function') { - return maybeTransform(range); - } else { - return maybeTransform; - } - } - - return range; -} - -function _validateForwardPeerDependencies( - name: string, - infoMap: Map, - peers: { [name: string]: string }, - peersMeta: { [name: string]: { optional?: boolean } }, - logger: logging.LoggerApi, - next: boolean, -): boolean { - let validationFailed = false; - for (const [peer, range] of Object.entries(peers)) { - logger.debug(`Checking forward peer ${peer}...`); - const maybePeerInfo = infoMap.get(peer); - const isOptional = peersMeta[peer] && !!peersMeta[peer].optional; - if (!maybePeerInfo) { - if (!isOptional) { - logger.warn( - [ - `Package ${JSON.stringify(name)} has a missing peer dependency of`, - `${JSON.stringify(peer)} @ ${JSON.stringify(range)}.`, - ].join(' '), - ); - } - - continue; - } - - const peerVersion = - maybePeerInfo.target && maybePeerInfo.target.packageJson.version - ? maybePeerInfo.target.packageJson.version - : maybePeerInfo.installed.version; - - logger.debug(` Range intersects(${range}, ${peerVersion})...`); - if (!semver.satisfies(peerVersion, range, { includePrerelease: next || undefined })) { - logger.error( - [ - `Package ${JSON.stringify(name)} has an incompatible peer dependency to`, - `${JSON.stringify(peer)} (requires ${JSON.stringify(range)},`, - `would install ${JSON.stringify(peerVersion)})`, - ].join(' '), - ); - - validationFailed = true; - continue; - } - } - - return validationFailed; -} - -function _validateReversePeerDependencies( - name: string, - version: string, - infoMap: Map, - logger: logging.LoggerApi, - next: boolean, -) { - for (const [installed, installedInfo] of infoMap.entries()) { - const installedLogger = logger.createChild(installed); - installedLogger.debug(`${installed}...`); - const peers = (installedInfo.target || installedInfo.installed).packageJson.peerDependencies; - - for (const [peer, range] of Object.entries(peers || {})) { - if (peer != name) { - // Only check peers to the packages we're updating. We don't care about peers - // that are unmet but we have no effect on. - continue; - } - - // Ignore peerDependency mismatches for these packages. - // They are deprecated and removed via a migration. - const ignoredPackages = [ - 'codelyzer', - '@schematics/update', - '@angular-devkit/build-ng-packagr', - 'tsickle', - '@nguniversal/builders', - ]; - if (ignoredPackages.includes(installed)) { - continue; - } - - // Override the peer version range if it's known as a compatible. - const extendedRange = _updatePeerVersion(infoMap, peer, range); - - if (!semver.satisfies(version, extendedRange, { includePrerelease: next || undefined })) { - logger.error( - [ - `Package ${JSON.stringify(installed)} has an incompatible peer dependency to`, - `${JSON.stringify(name)} (requires`, - `${JSON.stringify(range)}${extendedRange == range ? '' : ' (extended)'},`, - `would install ${JSON.stringify(version)}).`, - ].join(' '), - ); - - return true; - } - } - } - - return false; -} - -function _validateUpdatePackages( - infoMap: Map, - force: boolean, - next: boolean, - logger: logging.LoggerApi, -): void { - logger.debug('Updating the following packages:'); - infoMap.forEach((info) => { - if (info.target) { - logger.debug(` ${info.name} => ${info.target.version}`); - } - }); - - let peerErrors = false; - infoMap.forEach((info) => { - const { name, target } = info; - if (!target) { - return; - } - - const pkgLogger = logger.createChild(name); - logger.debug(`${name}...`); - - const { peerDependencies = {}, peerDependenciesMeta = {} } = target.packageJson; - peerErrors = - _validateForwardPeerDependencies( - name, - infoMap, - peerDependencies, - peerDependenciesMeta, - pkgLogger, - next, - ) || peerErrors; - peerErrors = - _validateReversePeerDependencies(name, target.version, infoMap, pkgLogger, next) || - peerErrors; - }); - - if (!force && peerErrors) { - throw new SchematicsException( - 'Incompatible peer dependencies found.\n' + - 'Peer dependency warnings when installing dependencies means that those dependencies might not work correctly together.\n' + - `You can use the '--force' option to ignore incompatible peer dependencies and instead address these warnings later.`, - ); - } -} - -function _performUpdate( - tree: Tree, - context: SchematicContext, - infoMap: Map, - logger: logging.LoggerApi, - migrateOnly: boolean, -): void { - const packageJsonContent = tree.read('/package.json')?.toString(); - if (!packageJsonContent) { - throw new SchematicsException('Could not find a package.json. Are you in a Node project?'); - } - - const packageJson = tree.readJson('/package.json') as PackageManifest; - - const updateDependency = (deps: Record, name: string, newVersion: string) => { - const oldVersion = deps[name]; - // We only respect caret and tilde ranges on update. - const execResult = /^[\^~]/.exec(oldVersion); - deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`; - }; - - const toInstall = [...infoMap.values()] - .map((x) => [x.name, x.target, x.installed]) - .filter(([name, target, installed]) => { - return !!name && !!target && !!installed; - }) as [string, PackageVersionInfo, PackageVersionInfo][]; - - toInstall.forEach(([name, target, installed]) => { - logger.info( - `Updating package.json with dependency ${name} ` + - `@ ${JSON.stringify(target.version)} (was ${JSON.stringify(installed.version)})...`, - ); - - if (packageJson.dependencies && packageJson.dependencies[name]) { - updateDependency(packageJson.dependencies, name, target.version); - - if (packageJson.devDependencies && packageJson.devDependencies[name]) { - delete packageJson.devDependencies[name]; - } - if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { - delete packageJson.peerDependencies[name]; - } - } else if (packageJson.devDependencies && packageJson.devDependencies[name]) { - updateDependency(packageJson.devDependencies, name, target.version); - - if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { - delete packageJson.peerDependencies[name]; - } - } else if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { - updateDependency(packageJson.peerDependencies, name, target.version); - } else { - logger.warn(`Package ${name} was not found in dependencies.`); - } - }); - const eofMatches = packageJsonContent.match(/\r?\n$/); - const eof = eofMatches?.[0] ?? ''; - const newContent = JSON.stringify(packageJson, null, 2) + eof; - if (packageJsonContent != newContent || migrateOnly) { - if (!migrateOnly) { - tree.overwrite('/package.json', newContent); - } - - const externalMigrations: {}[] = []; - - // Run the migrate schematics with the list of packages to use. The collection contains - // version information and we need to do this post installation. Please note that the - // migration COULD fail and leave side effects on disk. - // Run the schematics task of those packages. - toInstall.forEach(([name, target, installed]) => { - if (!target.updateMetadata.migrations) { - return; - } - - externalMigrations.push({ - package: name, - collection: target.updateMetadata.migrations, - from: installed.version, - to: target.version, - }); - - return; - }); - - if (externalMigrations.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (global as any).externalMigrations = externalMigrations; - } - } -} - -function _getUpdateMetadata( - packageJson: PackageManifest, - logger: logging.LoggerApi, -): UpdateMetadata { - const metadata = packageJson['ng-update']; - - const result: UpdateMetadata = { - packageGroup: {}, - requirements: {}, - }; - - if (!metadata || typeof metadata != 'object' || Array.isArray(metadata)) { - return result; - } - - if (metadata['packageGroup']) { - const packageGroup = metadata['packageGroup']; - // Verify that packageGroup is an array of strings or an map of versions. This is not an error - // but we still warn the user and ignore the packageGroup keys. - if (Array.isArray(packageGroup) && packageGroup.every((x) => typeof x == 'string')) { - result.packageGroup = packageGroup.reduce((group, name) => { - group[name] = packageJson.version; - - return group; - }, result.packageGroup); - } else if ( - typeof packageGroup == 'object' && - packageGroup && - !Array.isArray(packageGroup) && - Object.values(packageGroup).every((x) => typeof x == 'string') - ) { - result.packageGroup = packageGroup; - } else { - logger.warn(`packageGroup metadata of package ${packageJson.name} is malformed. Ignoring.`); - } - - result.packageGroupName = Object.keys(result.packageGroup)[0]; - } - - if (typeof metadata['packageGroupName'] == 'string') { - result.packageGroupName = metadata['packageGroupName']; - } - - if (metadata['migrations']) { - const migrations = metadata['migrations']; - if (typeof migrations != 'string') { - logger.warn(`migrations metadata of package ${packageJson.name} is malformed. Ignoring.`); - } else { - result.migrations = migrations; - } - } - - return result; -} - -function _usageMessage( - options: UpdateSchema, - infoMap: Map, - logger: logging.LoggerApi, -) { - const packageGroups = new Map(); - const packagesToUpdate = [...infoMap.entries()] - .map(([name, info]) => { - const distTags = info.npmPackageJson['dist-tags'] ?? {}; - let tag = options.next ? (distTags['next'] ? 'next' : 'latest') : 'latest'; - let version = distTags[tag] ?? info.installed.version; - const versions = info.npmPackageJson.versions ?? {}; - let target = versions[version]; - - const versionDiff = semver.diff(info.installed.version, version); - if ( - versionDiff !== 'patch' && - versionDiff !== 'minor' && - /^@(?:angular|nguniversal)\//.test(name) - ) { - const installedMajorVersion = semver.parse(info.installed.version)?.major; - const toInstallMajorVersion = semver.parse(version)?.major; - if ( - installedMajorVersion !== undefined && - toInstallMajorVersion !== undefined && - installedMajorVersion < toInstallMajorVersion - 1 - ) { - const nextMajorVersion = `${installedMajorVersion + 1}.`; - const nextMajorVersions = Object.keys(versions) - .filter((v) => v.startsWith(nextMajorVersion)) - .sort((a, b) => (a > b ? -1 : 1)); - - if (nextMajorVersions.length) { - version = nextMajorVersions[0]; - target = versions[version]; - tag = ''; - } - } - } - - return { - name, - info, - version, - tag, - target, - }; - }) - .filter( - ({ info, version, target }) => - target?.['ng-update'] && semver.compare(info.installed.version, version) < 0, - ) - .map(({ name, info, version, tag, target }) => { - // Look for packageGroup. - const ngUpdate = target['ng-update']; - const packageGroup = ngUpdate?.['packageGroup']; - if (packageGroup) { - const packageGroupNames = Array.isArray(packageGroup) - ? packageGroup - : Object.keys(packageGroup); - const packageGroupName = - ngUpdate?.['packageGroupName'] || packageGroupNames.find((n) => infoMap.has(n)); - - if (packageGroupName) { - if (packageGroups.has(name)) { - return null; - } - - for (const groupName of packageGroupNames) { - packageGroups.set(groupName, packageGroupName); - } - - packageGroups.set(packageGroupName, packageGroupName); - name = packageGroupName; - } - } - - let command = `ng update ${name}`; - if (!tag) { - command += `@${semver.parse(version)?.major || version}`; - } else if (tag == 'next') { - command += ' --next'; - } - - return [name, `${info.installed.version} -> ${version} `, command]; - }) - .filter((x) => x !== null) - .sort((a, b) => (a && b ? a[0].localeCompare(b[0]) : 0)); - - if (packagesToUpdate.length == 0) { - logger.info('We analyzed your package.json and everything seems to be in order. Good work!'); - - return; - } - - logger.info('We analyzed your package.json, there are some packages to update:\n'); - - // Find the largest name to know the padding needed. - let namePad = Math.max(...[...infoMap.keys()].map((x) => x.length)) + 2; - if (!Number.isFinite(namePad)) { - namePad = 30; - } - const pads = [namePad, 25, 0]; - - logger.info( - ' ' + ['Name', 'Version', 'Command to update'].map((x, i) => x.padEnd(pads[i])).join(''), - ); - - const totalWidth = pads.reduce((sum, width) => sum + width, 20); - logger.info(` ${'-'.repeat(totalWidth)}`); - - packagesToUpdate.forEach((fields) => { - if (!fields) { - return; - } - - logger.info(' ' + fields.map((x, i) => x.padEnd(pads[i])).join('')); - }); - - logger.info( - `\nThere might be additional packages which don't provide 'ng update' capabilities that are outdated.\n` + - `You can update the additional packages by running the update command of your package manager.`, - ); - - return; -} - -/** - * Resolves a semver range or npm dist-tag to a specific version based on the package's registry metadata. - * It prioritizes non-deprecated versions and handles fallback to deprecated versions if necessary. - * - * @private - */ -function resolvePackageVersion( - metadata: NpmRepositoryPackageJson, - range: string, - next = false, -): string | null { - // Check if range matches an npm dist-tag directly (e.g. "latest", "next") - const distTags = metadata['dist-tags'] ?? {}; - if (distTags[range]) { - return distTags[range]; - } - // If 'next' is requested (e.g. via the --next CLI flag) but the package doesn't publish - // a 'next' pre-release tag, fallback to 'latest'. - if (range === 'next') { - return distTags['latest'] ?? null; - } - - // Split deprecated and non-deprecated versions from registry metadata - const packageVersionsNonDeprecated: string[] = []; - const packageVersionsDeprecated: string[] = []; - for (const [v, { deprecated }] of Object.entries(metadata.versions ?? {})) { - if (deprecated) { - packageVersionsDeprecated.push(v); - } else { - packageVersionsNonDeprecated.push(v); - } - } - - // Find the highest satisfying version, prioritizing non-deprecated versions - return ( - semver.maxSatisfying(packageVersionsNonDeprecated, range, { - includePrerelease: next || undefined, - }) ?? - semver.maxSatisfying(packageVersionsDeprecated, range, { - includePrerelease: next || undefined, - }) - ); -} - -/** - * Checks if Yarn Plug'n'Play is active in the current workspace. - * - * @private - */ -function isPnpActive(workspaceRoot: string): boolean { - return ( - process.versions.pnp !== undefined || - existsSync(path.join(workspaceRoot, '.pnp.cjs')) || - existsSync(path.join(workspaceRoot, '.pnp.js')) - ); -} - -/** - * Resolves and reads the installed package.json manifest for a package. - * It checks the virtual schematic Tree first (vital for unit tests/mocks), - * and falls back to physical disk resolution using createRequire only if Yarn PnP is active. - * - * @private - */ -function getInstalledPackageJson( - tree: Tree, - packageName: string, - workspaceRoot: string, -): PackageManifest | null { - // First, check the virtual tree (critical for testing mocks) - const pkgJsonPath = `/node_modules/${packageName}/package.json`; - if (tree.exists(pkgJsonPath)) { - try { - return tree.readJson(pkgJsonPath) as PackageManifest; - } catch {} - } - - // In Yarn PnP, mock package trees are not written to node_modules in the virtual tree, - // so we resolve the manifest physically from Yarn's zip cache via createRequire. - // Note: This fallback resolution is strictly gated on Yarn PnP being active. Because schematics - // operate on a virtual file system (Tree), running disk lookups in non-PnP - // environments could cause tests to resolve dependencies from this monorepo's own node_modules - // instead of the simulated virtual file system. - if (isPnpActive(workspaceRoot)) { - try { - const workspaceRequire = createRequire(path.join(workspaceRoot, 'package.json')); - const manifestPath = workspaceRequire.resolve(`${packageName}/package.json`); - const content = readFileSync(manifestPath, 'utf8'); - - return JSON.parse(content) as PackageManifest; - } catch {} - } - - return null; -} - -function getInstalledVersion( - tree: Tree, - packageName: string, - workspaceRoot: string, -): string | null { - const pkgJson = getInstalledPackageJson(tree, packageName, workspaceRoot); - - return pkgJson?.version ?? null; -} - -function _buildLocalPackageInfo( - tree: Tree, - name: string, - allDependencies: ReadonlyMap, - workspaceRoot: string, - logger: logging.LoggerApi, -): PackageInfo { - const packageJsonRange = allDependencies.get(name); - if (!packageJsonRange) { - throw new SchematicsException(`Package ${JSON.stringify(name)} was not found in package.json.`); - } - - const localPkgJson = getInstalledPackageJson(tree, name, workspaceRoot); - if (!localPkgJson) { - throw new SchematicsException(`Package ${name} is not installed.`); - } - - return { - name, - npmPackageJson: {} as NpmRepositoryPackageJson, - installed: { - version: localPkgJson.version as VersionRange, - packageJson: localPkgJson, - updateMetadata: _getUpdateMetadata(localPkgJson, logger), - }, - packageJsonRange, - }; -} - -function _buildPackageInfo( - tree: Tree, - packages: Map, - allDependencies: ReadonlyMap, - npmPackageJson: NpmRepositoryPackageJson, - workspaceRoot: string, - logger: logging.LoggerApi, -): PackageInfo { - const name = npmPackageJson.name; - const packageJsonRange = allDependencies.get(name); - if (!packageJsonRange) { - throw new SchematicsException(`Package ${JSON.stringify(name)} was not found in package.json.`); - } - - const localPkgJson = getInstalledPackageJson(tree, name, workspaceRoot); - let installedVersion = localPkgJson?.version; - - const packageVersionsNonDeprecated: string[] = []; - const packageVersionsDeprecated: string[] = []; - - for (const [version, { deprecated }] of Object.entries(npmPackageJson.versions ?? {})) { - if (deprecated) { - packageVersionsDeprecated.push(version); - } else { - packageVersionsNonDeprecated.push(version); - } - } - - const findSatisfyingVersion = (targetVersion: VersionRange): VersionRange | undefined => - ((semver.maxSatisfying(packageVersionsNonDeprecated, targetVersion) ?? - semver.maxSatisfying(packageVersionsDeprecated, targetVersion)) as VersionRange | null) ?? - undefined; - - if (!installedVersion) { - // Find the version from NPM that fits the range to max. - installedVersion = findSatisfyingVersion(packageJsonRange); - } - - if (!installedVersion) { - throw new SchematicsException( - `An unexpected error happened; could not determine version for package ${name}.`, - ); - } - - const versions = npmPackageJson.versions ?? {}; - const installedPackageJson = versions[installedVersion] || localPkgJson; - if (!installedPackageJson) { - throw new SchematicsException( - `An unexpected error happened; package ${name} has no version ${installedVersion}.`, - ); - } - - let targetVersion: VersionRange | undefined = packages.get(name); - if (targetVersion) { - const distTags = npmPackageJson['dist-tags'] ?? {}; - if (distTags[targetVersion]) { - targetVersion = distTags[targetVersion] as VersionRange; - } else if (targetVersion == 'next') { - targetVersion = distTags['latest'] as VersionRange; - } else { - targetVersion = findSatisfyingVersion(targetVersion); - } - } - - if (targetVersion && semver.lte(targetVersion, installedVersion)) { - logger.debug(`Package ${name} already satisfied by package.json (${packageJsonRange}).`); - targetVersion = undefined; - } - - const target: PackageVersionInfo | undefined = targetVersion - ? { - version: targetVersion, - packageJson: versions[targetVersion], - updateMetadata: _getUpdateMetadata(versions[targetVersion], logger), - } - : undefined; - - return { - name, - npmPackageJson, - installed: { - version: installedVersion as VersionRange, - packageJson: installedPackageJson as PackageManifest, - updateMetadata: _getUpdateMetadata(installedPackageJson as PackageManifest, logger), - }, - target, - packageJsonRange, - }; -} - -function _buildPackageList( - options: UpdateSchema, - projectDeps: Map, - logger: logging.LoggerApi, -): Map { - // Parse the packages options to set the targeted version. - const packages = new Map(); - const commandLinePackages = - options.packages && options.packages.length > 0 ? options.packages : []; - - for (const pkg of commandLinePackages) { - // Split the version asked on command line. - const m = pkg.match(/^((?:@[^/]{1,100}\/)?[^@]{1,100})(?:@(.{1,100}))?$/); - if (!m) { - logger.warn(`Invalid package argument: ${JSON.stringify(pkg)}. Skipping.`); - continue; - } - - const [, npmName, maybeVersion] = m; - - const version = projectDeps.get(npmName); - if (!version) { - logger.warn(`Package not installed: ${JSON.stringify(npmName)}. Skipping.`); - continue; - } - - packages.set(npmName, (maybeVersion || (options.next ? 'next' : 'latest')) as VersionRange); - } - - return packages; -} - -function _addPackageGroup( - tree: Tree, - packages: Map, - allDependencies: ReadonlyMap, - npmPackageJson: NpmRepositoryPackageJson, - logger: logging.LoggerApi, -): void { - const maybePackage = packages.get(npmPackageJson.name); - if (!maybePackage) { - return; - } - - const distTags = npmPackageJson['dist-tags'] ?? {}; - let version = maybePackage; - if (distTags[version]) { - version = distTags[version] as VersionRange; - } else if (version === 'next') { - version = distTags['latest'] as VersionRange; - } else { - const packageVersionsNonDeprecated: string[] = []; - const packageVersionsDeprecated: string[] = []; - const versions = npmPackageJson.versions ?? {}; - for (const [v, { deprecated }] of Object.entries(versions)) { - if (deprecated) { - packageVersionsDeprecated.push(v); - } else { - packageVersionsNonDeprecated.push(v); - } - } - version = - ((semver.maxSatisfying(packageVersionsNonDeprecated, version) ?? - semver.maxSatisfying(packageVersionsDeprecated, version)) as VersionRange | null) ?? - version; - } - - const versions = npmPackageJson.versions ?? {}; - if (!versions[version]) { - return; - } - const ngUpdateMetadata = versions[version]['ng-update']; - if (!ngUpdateMetadata) { - return; - } - - const packageGroup = ngUpdateMetadata['packageGroup']; - if (!packageGroup) { - return; - } - let packageGroupNormalized: Record; - if (Array.isArray(packageGroup) && !packageGroup.some((x) => typeof x != 'string')) { - packageGroupNormalized = packageGroup.reduce( - (acc, curr) => { - acc[curr] = maybePackage; - - return acc; - }, - {} as { [name: string]: string }, - ); - } else if ( - typeof packageGroup == 'object' && - packageGroup && - !Array.isArray(packageGroup) && - Object.values(packageGroup).every((x) => typeof x == 'string') - ) { - packageGroupNormalized = packageGroup; - } else { - logger.warn(`packageGroup metadata of package ${npmPackageJson.name} is malformed. Ignoring.`); - - return; - } - - for (const [name, value] of Object.entries(packageGroupNormalized)) { - // Don't override names from the command line. - // Remove packages that aren't installed. - if (!packages.has(name) && allDependencies.has(name)) { - packages.set(name, value as VersionRange); - } - } -} - -/** - * Add peer dependencies of packages on the command line to the list of packages to update. - * We don't do verification of the versions here as this will be done by a later step (and can - * be ignored by the --force flag). - * @private - */ -async function _addPeerDependencies( - tree: Tree, - packages: Map, - allDependencies: ReadonlyMap, - npmPackageJson: NpmRepositoryPackageJson, - workspaceRoot: string, - fetchMetadata: (name: string) => Promise, - logger: logging.LoggerApi, -): Promise { - const maybePackage = packages.get(npmPackageJson.name); - if (!maybePackage) { - return; - } - const distTags = npmPackageJson['dist-tags'] ?? {}; - const version = distTags[maybePackage] || maybePackage; - const versions = npmPackageJson.versions ?? {}; - const packageJson = versions[version]; - if (!packageJson) { - return; - } - - for (const [peer, range] of Object.entries(packageJson.peerDependencies || {})) { - if (packages.has(peer)) { - continue; - } - - const installedVersion = getInstalledVersion(tree, peer, workspaceRoot); - if (installedVersion) { - if (semver.satisfies(installedVersion, range)) { - continue; - } - } else { - const packageJsonRange = allDependencies.get(peer); - if (packageJsonRange) { - const peerMetadata = await fetchMetadata(peer); - if (peerMetadata) { - const packageVersionsNonDeprecated: string[] = []; - const packageVersionsDeprecated: string[] = []; - for (const [v, { deprecated }] of Object.entries(peerMetadata.versions ?? {})) { - if (deprecated) { - packageVersionsDeprecated.push(v); - } else { - packageVersionsNonDeprecated.push(v); - } - } - const resolvedInstalledVersion = - semver.maxSatisfying(packageVersionsNonDeprecated, packageJsonRange) ?? - semver.maxSatisfying(packageVersionsDeprecated, packageJsonRange); - - if (resolvedInstalledVersion && semver.satisfies(resolvedInstalledVersion, range)) { - continue; - } - } - } - } - - packages.set(peer, range as VersionRange); - } -} - -function _getAllDependencies(tree: Tree): Array { - const { dependencies, devDependencies, peerDependencies } = tree.readJson( - '/package.json', - ) as PackageManifest; - - return [ - ...(Object.entries(peerDependencies || {}) as Array<[string, VersionRange]>), - ...(Object.entries(devDependencies || {}) as Array<[string, VersionRange]>), - ...(Object.entries(dependencies || {}) as Array<[string, VersionRange]>), - ]; -} - -function _formatVersion(version: string | undefined) { - if (version === undefined) { - return undefined; - } - - if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { - version += '.0'; - } - if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { - version += '.0'; - } - if (!semver.valid(version)) { - throw new SchematicsException(`Invalid migration version: ${JSON.stringify(version)}`); - } - - return version; -} - -/** - * Returns whether or not the given package specifier (the value string in a - * `package.json` dependency) is hosted in the NPM registry. - * @throws When the specifier cannot be parsed. - */ -function isPkgFromRegistry(name: string, specifier: string): boolean { - const result = npa.resolve(name, specifier); - - return !!result.registry; -} - -export default function (options: UpdateSchema): Rule { - if (!options.packages) { - // We cannot just return this because we need to fetch the packages from NPM still for the - // help/guide to show. - options.packages = []; - } else { - // We split every packages by commas to allow people to pass in multiple and make it an array. - options.packages = options.packages.reduce((acc, curr) => { - return acc.concat(curr.split(',')); - }, [] as string[]); - } - - if (options.migrateOnly && options.from) { - if (options.packages.length !== 1) { - throw new SchematicsException('--from requires that only a single package be passed.'); - } - } - - options.from = _formatVersion(options.from); - options.to = _formatVersion(options.to); - const usingYarn = options.packageManager === 'yarn'; - - return async (tree: Tree, context: SchematicContext) => { - const logger = context.logger; - const npmDeps = new Map( - _getAllDependencies(tree).filter(([name, specifier]) => { - try { - return isPkgFromRegistry(name, specifier); - } catch { - logger.warn(`Package ${name} was not found on the registry. Skipping.`); - - return false; - } - }), - ); - const packages = _buildPackageList(options, npmDeps, logger); - - const workspaceRoot = options.workspaceRoot ?? process.cwd(); - const npmPackageJsonMap = new Map(); - - const getOrFetchPackageMetadata = async ( - packageName: string, - ): Promise => { - let metadata = npmPackageJsonMap.get(packageName); - if (!metadata) { - const raw = await getNpmPackageJson(packageName, logger, { - registry: options.registry, - usingYarn, - verbose: options.verbose, - }); - if (raw.name) { - metadata = raw as NpmRepositoryPackageJson; - npmPackageJsonMap.set(packageName, metadata); - } - } - - return metadata ?? null; - }; - - if (packages.size === 0) { - // User ran just `ng update` to see the outdated package list. - // We must fetch metadata for all npm dependencies to generate the usage message. - await Promise.all( - Array.from(npmDeps.keys()).map(async (depName) => { - await getOrFetchPackageMetadata(depName); - }), - ); - } else { - // User requested updates. We resolve dependencies lazily. - let lastPackagesSize; - do { - lastPackagesSize = packages.size; - - let lastGroupSize; - do { - lastGroupSize = packages.size; - for (const name of Array.from(packages.keys())) { - const metadata = await getOrFetchPackageMetadata(name); - const spec = packages.get(name); - if (metadata && spec) { - const resolvedVersion = resolvePackageVersion(metadata, spec, !!options.next); - if (resolvedVersion) { - packages.set(name, resolvedVersion as VersionRange); - } - _addPackageGroup(tree, packages, npmDeps, metadata, logger); - } - } - } while (packages.size > lastGroupSize); - - for (const name of Array.from(packages.keys())) { - const metadata = await getOrFetchPackageMetadata(name); - const spec = packages.get(name); - if (metadata && spec) { - const resolvedVersion = resolvePackageVersion(metadata, spec, !!options.next); - if (resolvedVersion) { - packages.set(name, resolvedVersion as VersionRange); - } - await _addPeerDependencies( - tree, - packages, - npmDeps, - metadata, - workspaceRoot, - getOrFetchPackageMetadata, - logger, - ); - } - } - } while (packages.size > lastPackagesSize); - } - - // Build the PackageInfo for each module. - const packageInfoMap = new Map(); - for (const depName of npmDeps.keys()) { - const isUpdating = packages.has(depName); - const localPkgJson = getInstalledPackageJson(tree, depName, workspaceRoot); - - if (isUpdating || !localPkgJson) { - // If updating OR not installed locally, resolve via registry metadata - const metadata = await getOrFetchPackageMetadata(depName); - if (metadata) { - packageInfoMap.set( - depName, - _buildPackageInfo(tree, packages, npmDeps, metadata, workspaceRoot, logger), - ); - } else { - // Fallback if metadata could not be fetched - packageInfoMap.set( - depName, - _buildLocalPackageInfo(tree, depName, npmDeps, workspaceRoot, logger), - ); - } - } else { - // If not updating and installed locally, resolve purely locally - packageInfoMap.set( - depName, - _buildLocalPackageInfo(tree, depName, npmDeps, workspaceRoot, logger), - ); - } - } - - // Now that we have all the information, check the flags. - if (packages.size > 0) { - if (options.migrateOnly && options.from && options.packages) { - return; - } - - const sublog = new logging.LevelCapLogger('validation', logger.createChild(''), 'warn'); - _validateUpdatePackages(packageInfoMap, !!options.force, !!options.next, sublog); - - _performUpdate(tree, context, packageInfoMap, logger, !!options.migrateOnly); - } else { - _usageMessage(options, packageInfoMap, logger); - } - }; -} diff --git a/packages/angular/cli/src/commands/update/schematic/index_spec.ts b/packages/angular/cli/src/commands/update/schematic/index_spec.ts deleted file mode 100644 index 7e8ca436150d..000000000000 --- a/packages/angular/cli/src/commands/update/schematic/index_spec.ts +++ /dev/null @@ -1,391 +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 { normalize, virtualFs } from '@angular-devkit/core'; -import { HostTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; -import * as semver from 'semver'; -import { angularMajorCompatGuarantee } from './index'; - -describe('angularMajorCompatGuarantee', () => { - [ - '5.0.0', - '5.1.0', - '5.20.0', - '6.0.0', - '6.0.0-rc.0', - '6.0.0-beta.0', - '6.1.0-beta.0', - '6.1.0-rc.0', - '6.10.11', - ].forEach((golden) => { - it('works with ' + JSON.stringify(golden), () => { - expect(semver.satisfies(golden, angularMajorCompatGuarantee('^5.0.0'))).toBeTruthy(); - }); - }); -}); - -describe('@schematics/update', () => { - const schematicRunner = new SchematicTestRunner( - '@schematics/update', - require.resolve('./collection.json'), - ); - let host: virtualFs.test.TestHost; - let appTree: UnitTestTree = new UnitTestTree(new HostTree()); - - beforeEach(() => { - host = new virtualFs.test.TestHost({ - '/package.json': `{ - "name": "blah", - "dependencies": { - "@angular-devkit-tests/update-base": "1.0.0" - } - }`, - }); - appTree = new UnitTestTree(new HostTree(host)); - }); - - it('ignores dependencies not hosted on the NPM registry', async () => { - let newTree = new UnitTestTree( - new HostTree( - new virtualFs.test.TestHost({ - '/package.json': `{ - "name": "blah", - "dependencies": { - "@angular-devkit-tests/update-base": "file:update-base-1.0.0.tgz" - } - }`, - }), - ), - ); - - newTree = await schematicRunner.runSchematic('update', undefined, newTree); - const packageJson = JSON.parse(newTree.readContent('/package.json')); - expect(packageJson['dependencies']['@angular-devkit-tests/update-base']).toBe( - 'file:update-base-1.0.0.tgz', - ); - }, 45000); - - it('should not error with yarn 2.0 protocols', async () => { - let newTree = new UnitTestTree( - new HostTree( - new virtualFs.test.TestHost({ - '/package.json': `{ - "name": "blah", - "dependencies": { - "src": "src@link:./src", - "@angular-devkit-tests/update-base": "1.0.0" - } - }`, - }), - ), - ); - - newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular-devkit-tests/update-base'], - }, - newTree, - ); - const { dependencies } = JSON.parse(newTree.readContent('/package.json')); - expect(dependencies['@angular-devkit-tests/update-base']).toBe('1.1.0'); - }); - - it('updates Angular as compatible with Angular N-1', async () => { - // Add the basic migration package. - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - const dependencies = packageJson['dependencies']; - dependencies['@angular-devkit-tests/update-peer-dependencies-angular-5'] = '1.0.0'; - dependencies['@angular/core'] = '5.1.0'; - dependencies['rxjs'] = '5.5.0'; - dependencies['zone.js'] = '0.8.26'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular/core@^6.0.0'], - }, - appTree, - ); - const newPpackageJson = JSON.parse(newTree.readContent('/package.json')); - expect(newPpackageJson['dependencies']['@angular/core'][0]).toBe('6'); - }, 45000); - - it('updates Angular as compatible with Angular N-1 (2)', async () => { - // Add the basic migration package. - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - const dependencies = packageJson['dependencies']; - dependencies['@angular-devkit-tests/update-peer-dependencies-angular-5-2'] = '1.0.0'; - dependencies['@angular/core'] = '5.1.0'; - dependencies['@angular/animations'] = '5.1.0'; - dependencies['@angular/common'] = '5.1.0'; - dependencies['@angular/compiler'] = '5.1.0'; - dependencies['@angular/compiler-cli'] = '5.1.0'; - dependencies['@angular/platform-browser'] = '5.1.0'; - dependencies['rxjs'] = '5.5.0'; - dependencies['zone.js'] = '0.8.26'; - dependencies['typescript'] = '2.4.2'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular/core@^6.0.0'], - }, - appTree, - ); - - const newPackageJson = JSON.parse(newTree.readContent('/package.json')); - expect(newPackageJson['dependencies']['@angular/core'][0]).toBe('6'); - expect(newPackageJson['dependencies']['rxjs'][0]).toBe('6'); - expect(newPackageJson['dependencies']['typescript'][0]).toBe('2'); - expect(newPackageJson['dependencies']['typescript'][2]).not.toBe('4'); - }, 45000); - - it('uses packageGroup for versioning', async () => { - // Add the basic migration package. - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - const dependencies = packageJson['dependencies']; - dependencies['@angular-devkit-tests/update-package-group-1'] = '1.0.0'; - dependencies['@angular-devkit-tests/update-package-group-2'] = '1.0.0'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular-devkit-tests/update-package-group-1'], - }, - appTree, - ); - const { dependencies: deps } = JSON.parse(newTree.readContent('/package.json')); - expect(deps['@angular-devkit-tests/update-package-group-1']).toBe('1.2.0'); - expect(deps['@angular-devkit-tests/update-package-group-2']).toBe('2.0.0'); - }, 45000); - - it('can migrate only', async () => { - // Add the basic migration package. - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.0.0'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular-devkit-tests/update-migrations'], - migrateOnly: true, - }, - appTree, - ); - - const newPackageJson = JSON.parse(newTree.readContent('/package.json')); - expect(newPackageJson['dependencies']['@angular-devkit-tests/update-base']).toBe('1.0.0'); - expect(newPackageJson['dependencies']['@angular-devkit-tests/update-migrations']).toBe('1.0.0'); - }, 45000); - - it('can migrate from only', async () => { - // Add the basic migration package. - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.6.0'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular-devkit-tests/update-migrations'], - migrateOnly: true, - from: '0.1.2', - }, - appTree, - ); - const { dependencies } = JSON.parse(newTree.readContent('/package.json')); - expect(dependencies['@angular-devkit-tests/update-migrations']).toBe('1.6.0'); - }, 45000); - - it('can install and migrate with --from (short version number)', async () => { - // Add the basic migration package. - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.6.0'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const newTree = await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular-devkit-tests/update-migrations'], - migrateOnly: true, - from: '0', - }, - appTree, - ); - const { dependencies } = JSON.parse(newTree.readContent('/package.json')); - expect(dependencies['@angular-devkit-tests/update-migrations']).toBe('1.6.0'); - }, 45000); - - it('validates peer dependencies', async () => { - const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); - const packageJson = JSON.parse(content); - const dependencies = packageJson['dependencies']; - // TODO: when we start using a local npm registry for test packages, add a package that includes - // a optional peer dependency and a non-optional one for this test. Use it instead of - // @angular-devkit/build-angular, whose optional peerdep is @angular/localize and non-optional - // are typescript and @angular/compiler-cli. - dependencies['@angular-devkit/build-angular'] = '0.900.0-next.1'; - host.sync.write( - normalize('/package.json'), - virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), - ); - - const messages: string[] = []; - schematicRunner.logger.subscribe((x) => messages.push(x.message)); - const hasPeerdepMsg = (dep: string) => - messages.some((str) => str.includes(`missing peer dependency of "${dep}"`)); - - await schematicRunner.runSchematic( - 'update', - { - packages: ['@angular-devkit/build-angular'], - next: true, - }, - appTree, - ); - expect(hasPeerdepMsg('@angular/compiler-cli')).toBeTruthy(); - expect(hasPeerdepMsg('typescript')).toBeTruthy(); - expect(hasPeerdepMsg('@angular/localize')).toBeFalsy(); - }, 45000); - - it('does not remove newline at the end of package.json', async () => { - const newlineStyles = ['\n', '\r\n']; - for (const newline of newlineStyles) { - const packageJsonContent = `{ - "name": "blah", - "dependencies": { - "@angular-devkit-tests/update-base": "1.0.0" - } - }${newline}`; - const inputTree = new UnitTestTree( - new HostTree( - new virtualFs.test.TestHost({ - '/package.json': packageJsonContent, - }), - ), - ); - - const resultTree = await schematicRunner.runSchematic( - 'update', - { packages: ['@angular-devkit-tests/update-base'] }, - inputTree, - ); - - const resultTreeContent = resultTree.readContent('/package.json'); - expect(resultTreeContent.endsWith(newline)).toBeTrue(); - } - }); - - it('does not add a newline at the end of package.json', async () => { - const packageJsonContent = `{ - "name": "blah", - "dependencies": { - "@angular-devkit-tests/update-base": "1.0.0" - } - }`; - const inputTree = new UnitTestTree( - new HostTree( - new virtualFs.test.TestHost({ - '/package.json': packageJsonContent, - }), - ), - ); - - const resultTree = await schematicRunner.runSchematic( - 'update', - { packages: ['@angular-devkit-tests/update-base'] }, - inputTree, - ); - - const resultTreeContent = resultTree.readContent('/package.json'); - expect(resultTreeContent.endsWith('}')).toBeTrue(); - }); - - it('updates group members to the same version as the targeted package', async () => { - const packageJsonContent = `{ - "name": "test", - "dependencies": { - "@angular/cdk": "^19.2.19", - "@angular/common": "^19.2.0", - "@angular/compiler": "^19.2.0", - "@angular/core": "^19.2.0", - "@angular/forms": "^19.2.0", - "@angular/platform-browser": "^19.2.0", - "@angular/platform-browser-dynamic": "^19.2.0", - "@angular/router": "^19.2.0", - "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.15.0" - }, - "devDependencies": { - "@angular-devkit/build-angular": "^19.2.21", - "@angular/cli": "^19.2.21", - "@angular/compiler-cli": "^19.2.0", - "typescript": "~5.7.2" - } - }`; - - const inputTree = new UnitTestTree( - new HostTree( - new virtualFs.test.TestHost({ - '/package.json': packageJsonContent, - }), - ), - ); - - const resultTree = await schematicRunner.runSchematic( - 'update', - { force: true, packages: ['@angular/cli@20', '@angular/cdk@20', '@angular/core@20'] }, - inputTree, - ); - - const { devDependencies, dependencies } = resultTree.readJson('/package.json') as { - devDependencies: Record; - dependencies: Record; - }; - - const version20Regexp = /^\^20.\d+.\d+$/; - - expect(devDependencies['typescript']).toMatch(/5\.9\.\d+/); - expect(devDependencies['@angular/cli']).toMatch(version20Regexp); - expect(devDependencies['@angular/compiler-cli']).toMatch(version20Regexp); - expect(dependencies['@angular/cdk']).toMatch(version20Regexp); - expect(dependencies['@angular/common']).toMatch(version20Regexp); - expect(dependencies['@angular/core']).toMatch(version20Regexp); - }, 45000); -}); diff --git a/packages/angular/cli/src/commands/update/schematic/schema.json b/packages/angular/cli/src/commands/update/schematic/schema.json deleted file mode 100644 index 63bf2df87813..000000000000 --- a/packages/angular/cli/src/commands/update/schematic/schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "SchematicsUpdateSchema", - "title": "Schematic Options Schema", - "type": "object", - "properties": { - "packages": { - "description": "The package or packages to update.", - "type": "array", - "items": { - "type": "string" - }, - "$default": { - "$source": "argv" - } - }, - "force": { - "description": "When false (the default), reports an error if installed packages are incompatible with the update.", - "default": false, - "type": "boolean" - }, - "next": { - "description": "Update to the latest version, including beta and RCs.", - "default": false, - "type": "boolean" - }, - "migrateOnly": { - "description": "Perform a migration, but do not update the installed version.", - "default": false, - "type": "boolean" - }, - "from": { - "description": "When using `--migrateOnly` for a single package, the version of that package from which to migrate.", - "type": "string" - }, - "to": { - "description": "When using `--migrateOnly` for a single package, the version of that package to which to migrate.", - "type": "string" - }, - "registry": { - "description": "The npm registry to use.", - "type": "string", - "oneOf": [ - { - "format": "uri" - }, - { - "format": "hostname" - } - ] - }, - "verbose": { - "description": "Display additional details during the update process.", - "type": "boolean" - }, - "packageManager": { - "description": "The preferred package manager configuration files to use for registry settings.", - "type": "string", - "default": "npm", - "enum": ["npm", "yarn", "pnpm", "bun"] - }, - "workspaceRoot": { - "description": "The path to the workspace root directory.", - "type": "string" - } - }, - "required": [] -} diff --git a/packages/angular/cli/src/commands/update/update-resolver.ts b/packages/angular/cli/src/commands/update/update-resolver.ts new file mode 100644 index 000000000000..995762d92bc9 --- /dev/null +++ b/packages/angular/cli/src/commands/update/update-resolver.ts @@ -0,0 +1,1257 @@ +/** + * @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 { logging } from '@angular-devkit/core'; +import { existsSync, promises as fs, readFileSync, realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import * as path from 'node:path'; +import npa from 'npm-package-arg'; +import * as semver from 'semver'; +import type { PackageManager, PackageManifest, PackageMetadata } from '../../package-managers'; + +export type VersionRange = string & { __VERSION_RANGE: void }; +type PeerVersionTransform = string | ((range: string) => string); + +export class RegistryClient { + private metadataCache = new Map>(); + private manifestCache = new Map>(); + + constructor( + private packageManager: PackageManager, + private logger: logging.LoggerApi, + readonly minReleaseAge: number = 0, + private getRegistryName?: (name: string) => string, + ) {} + + async getMetadata(packageName: string): Promise { + const registryName = this.getRegistryName ? this.getRegistryName(packageName) : packageName; + let promise = this.metadataCache.get(registryName); + if (!promise) { + promise = this.packageManager.getRegistryMetadata(registryName).catch((e) => { + this.metadataCache.delete(registryName); + throw e; + }); + this.metadataCache.set(registryName, promise); + } + + const metadata = await promise; + if (metadata && registryName !== packageName) { + return { ...metadata, name: packageName }; + } + + return metadata; + } + + async getManifest(packageName: string, version: string): Promise { + const registryName = this.getRegistryName ? this.getRegistryName(packageName) : packageName; + const key = `${registryName}@${version}`; + let promise = this.manifestCache.get(key); + if (!promise) { + promise = this.packageManager.getRegistryManifest(registryName, version).catch((e) => { + this.manifestCache.delete(key); + throw e; + }); + this.manifestCache.set(key, promise); + } + + const manifest = await promise; + if (manifest && registryName !== packageName) { + return { ...manifest, name: packageName }; + } + + return manifest; + } +} + +function isReleaseAgeSatisfied( + registryClient: RegistryClient, + metadata: PackageMetadata, + version: string, +): boolean { + const minReleaseAge = registryClient.minReleaseAge; + if (!minReleaseAge || !metadata.time) { + return true; + } + + const publishTimeStr = metadata.time[version]; + if (!publishTimeStr) { + return true; + } + + const publishTime = Date.parse(publishTimeStr); + if (isNaN(publishTime)) { + return true; + } + + return Date.now() - publishTime >= minReleaseAge; +} + +export async function getSatisfyingVersion( + registryClient: RegistryClient, + metadata: PackageMetadata, + range: string, + next?: boolean, +): Promise { + const options = { includePrerelease: next || undefined }; + let candidates = metadata.versions.filter((v) => semver.satisfies(v, range, options)); + + candidates = candidates.filter((version) => + isReleaseAgeSatisfied(registryClient, metadata, version), + ); + + const sorted = semver.rsort(candidates); + + for (const version of sorted) { + const manifest = await registryClient.getManifest(metadata.name, version); + if (manifest && !manifest.deprecated) { + return version; + } + } + + // Fallback to deprecated versions if no non-deprecated version satisfies + for (const version of sorted) { + const manifest = await registryClient.getManifest(metadata.name, version); + if (manifest) { + return version; + } + } + + return null; +} + +export function angularMajorCompatGuarantee(range: string) { + let newRange = semver.validRange(range); + if (!newRange) { + return range; + } + let major = 1; + while (!semver.gtr(major + '.0.0', newRange)) { + major++; + if (major >= 99) { + return newRange; + } + } + + newRange = range; + for (let minor = 0; minor < 20; minor++) { + newRange += ` || ^${major}.${minor}.0-alpha.0 `; + } + + return semver.validRange(newRange) || range; +} + +const knownPeerCompatibleList: { [name: string]: PeerVersionTransform } = { + '@angular/core': angularMajorCompatGuarantee, +}; + +export interface PackageVersionInfo { + version: VersionRange; + packageJson: PackageManifest; + updateMetadata: UpdateMetadata; +} + +export interface PackageInfo { + name: string; + npmPackageJson: PackageMetadata; + installed: PackageVersionInfo; + target?: PackageVersionInfo; + packageJsonRange: string; +} + +export interface UpdateMetadata { + packageGroupName?: string; + packageGroup: { [packageName: string]: string }; + requirements: { [packageName: string]: string }; + migrations?: string; +} + +export interface UpdateResolverOptions { + packages?: string[]; + force?: boolean; + next?: boolean; + migrateOnly?: boolean; + from?: string; + to?: string; + registry?: string; + packageManager?: string; + verbose?: boolean; + workspaceRoot?: string; +} + +export interface UpdatePlan { + packagesToUpdate: Map; // name -> target version range + migrationsToRun: { package: string; collection: string; from: string; to: string }[]; + packageInfoMap: Map; + registryClient: RegistryClient; +} + +function _updatePeerVersion(infoMap: Map, name: string, range: string) { + const maybePackageInfo = infoMap.get(name); + if (!maybePackageInfo) { + return range; + } + if (maybePackageInfo.target) { + name = maybePackageInfo.target.updateMetadata.packageGroupName || name; + } else { + name = maybePackageInfo.installed.updateMetadata.packageGroupName || name; + } + + const maybeTransform = knownPeerCompatibleList[name]; + if (maybeTransform) { + if (typeof maybeTransform == 'function') { + return maybeTransform(range); + } else { + return maybeTransform; + } + } + + return range; +} + +function _validateForwardPeerDependencies( + name: string, + infoMap: Map, + logger: logging.LoggerApi, +): boolean { + let error = false; + const info = infoMap.get(name); + if (!info || !info.target) { + return error; + } + + const peerDependencies = info.target.packageJson.peerDependencies || {}; + const peerDependenciesMeta = info.target.packageJson.peerDependenciesMeta || {}; + + for (const [peer, range] of Object.entries(peerDependencies)) { + const peerInfo = infoMap.get(peer); + if (!peerInfo) { + continue; + } + + const isOptional = !!peerDependenciesMeta[peer]?.optional; + const resolvedRange = _updatePeerVersion(infoMap, peer, range); + const resolvedVersion = peerInfo.target ? peerInfo.target.version : peerInfo.installed.version; + + if (!semver.satisfies(resolvedVersion, resolvedRange, { includePrerelease: true })) { + logger.error( + `Package ${JSON.stringify(name)} has an incompatible peer dependency to ` + + `${JSON.stringify(peer)} (requires ${JSON.stringify(range)}, ` + + `would install ${JSON.stringify(resolvedVersion)}).`, + ); + error = error || !isOptional; + } + } + + return error; +} + +function _validateReversePeerDependencies( + name: string, + version: string, + infoMap: Map, + logger: logging.LoggerApi, + next: boolean, +): boolean { + let error = false; + for (const [installed, installedInfo] of infoMap.entries()) { + const installedLogger = logger.createChild(installed); + installedLogger.debug(`${installed}...`); + const peers = (installedInfo.target || installedInfo.installed).packageJson.peerDependencies; + const peersMeta = (installedInfo.target || installedInfo.installed).packageJson + .peerDependenciesMeta; + + for (const [peer, range] of Object.entries(peers || {})) { + if (peer !== name) { + continue; + } + + const isOptional = !!peersMeta?.[peer]?.optional; + const resolvedRange = _updatePeerVersion(infoMap, name, range); + if (!semver.satisfies(version, resolvedRange, { includePrerelease: next || undefined })) { + logger.error( + `Package ${JSON.stringify(installed)} has an incompatible peer dependency to ` + + `${JSON.stringify(name)} (requires ${JSON.stringify(range)}, ` + + `would install ${JSON.stringify(version)}).`, + ); + error = error || !isOptional; + } + } + } + + return error; +} + +function _validateUpdatePackages( + infoMap: Map, + force: boolean, + next: boolean, + logger: logging.LoggerApi, +): void { + logger.debug('Validating peer dependencies...'); + let error = false; + + for (const name of infoMap.keys()) { + const info = infoMap.get(name); + if (!info || !info.target) { + continue; + } + + logger.debug(`Checking ${name}...`); + error = _validateForwardPeerDependencies(name, infoMap, logger) || error; + error = + _validateReversePeerDependencies(name, info.target.version, infoMap, logger, next) || error; + } + + if (error && !force) { + throw new Error( + 'Incompatible peer dependencies found. See above for details. ' + + 'You can bypass this check using the --force option.', + ); + } +} + +function _getUpdateMetadata( + packageJson: PackageManifest, + logger: logging.LoggerApi, +): UpdateMetadata { + const metadata = packageJson['ng-update'] as Record | undefined; + + const result: UpdateMetadata = { + packageGroup: {}, + requirements: {}, + }; + + if (!metadata || typeof metadata != 'object' || Array.isArray(metadata)) { + return result; + } + + if (metadata['packageGroup']) { + const packageGroup = metadata['packageGroup']; + if (Array.isArray(packageGroup) && packageGroup.every((x) => typeof x == 'string')) { + result.packageGroup = packageGroup.reduce( + (group, name) => { + group[name] = packageJson.version; + + return group; + }, + {} as { [key: string]: string }, + ); + } else if (typeof packageGroup == 'object' && packageGroup !== null) { + result.packageGroup = Object.entries(packageGroup).reduce( + (group, [name, version]) => { + if (typeof version == 'string') { + group[name] = version; + } + + return group; + }, + {} as { [key: string]: string }, + ); + } else { + logger.warn(`PackageGroup metadata for ${packageJson.name} is malformed. Ignoring.`); + } + } + + if (typeof metadata['packageGroupName'] == 'string') { + result.packageGroupName = metadata['packageGroupName']; + } + + if (typeof metadata['migrations'] == 'string') { + result.migrations = metadata['migrations']; + } + + return result; +} + +export function isPnpActive(workspaceRoot: string): boolean { + return ( + process.versions.pnp !== undefined || + existsSync(path.join(workspaceRoot, '.pnp.cjs')) || + existsSync(path.join(workspaceRoot, '.pnp.js')) + ); +} + +export function findPackageJson(workspaceDir: string, packageName: string): string | undefined { + if (isPnpActive(workspaceDir)) { + try { + const workspaceRequire = createRequire(path.join(workspaceDir, 'package.json')); + + return workspaceRequire.resolve(`${packageName}/package.json`); + } catch { + return undefined; + } + } + + let currentDir = workspaceDir; + while (true) { + const candidatePath = path.join(currentDir, 'node_modules', packageName, 'package.json'); + if (existsSync(candidatePath)) { + return realpathSync(candidatePath); + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + + return undefined; +} + +function getInstalledPackageJson( + packageName: string, + workspaceRoot: string, +): PackageManifest | null { + try { + const manifestPath = findPackageJson(workspaceRoot, packageName); + if (manifestPath) { + const content = readFileSync(manifestPath, 'utf8'); + + return JSON.parse(content) as PackageManifest; + } + } catch {} + + return null; +} + +function getInstalledVersion(packageName: string, workspaceRoot: string): string | null { + const pkgJson = getInstalledPackageJson(packageName, workspaceRoot); + + return pkgJson?.version ?? null; +} + +function _buildLocalPackageInfo( + name: string, + allDependencies: ReadonlyMap, + workspaceRoot: string, +): PackageInfo { + const packageJsonRange = allDependencies.get(name); + if (!packageJsonRange) { + throw new Error(`Package ${JSON.stringify(name)} was not found in package.json.`); + } + + const localPkgJson = getInstalledPackageJson(name, workspaceRoot); + if (!localPkgJson) { + throw new Error(`Package ${name} is not installed.`); + } + + const installedVersion = localPkgJson.version; + const npmPackageJson: PackageMetadata = { + name, + versions: [installedVersion], + 'dist-tags': {}, + }; + + const logger = new logging.NullLogger(); + + return { + name, + npmPackageJson, + installed: { + version: installedVersion as VersionRange, + packageJson: localPkgJson, + updateMetadata: _getUpdateMetadata(localPkgJson, logger), + }, + packageJsonRange, + }; +} + +async function _buildPackageInfo( + packages: Map, + allDependencies: ReadonlyMap, + npmPackageJson: PackageMetadata, + workspaceRoot: string, + registryClient: RegistryClient, + logger: logging.LoggerApi, +): Promise { + const name = npmPackageJson.name; + const packageJsonRange = allDependencies.get(name); + if (!packageJsonRange) { + throw new Error(`Package ${JSON.stringify(name)} was not found in package.json.`); + } + + const localPkgJson = getInstalledPackageJson(name, workspaceRoot); + let installedVersion = localPkgJson?.version; + + if (!installedVersion) { + installedVersion = (await getSatisfyingVersion( + registryClient, + npmPackageJson, + packageJsonRange, + )) as VersionRange | undefined; + } + + if (!installedVersion) { + throw new Error( + `An unexpected error happened; could not determine version for package ${name}.`, + ); + } + + const installedPackageJson = + localPkgJson || (await registryClient.getManifest(name, installedVersion)); + if (!installedPackageJson) { + throw new Error( + `An unexpected error happened; package ${name} has no version ${installedVersion}.`, + ); + } + + let targetVersion: VersionRange | undefined = packages.get(name); + if (targetVersion) { + const distTags = npmPackageJson['dist-tags'] ?? {}; + let resolvedVersion: string | undefined = + distTags[targetVersion] ?? (targetVersion === 'next' ? distTags['latest'] : undefined); + + if ( + resolvedVersion && + !isReleaseAgeSatisfied(registryClient, npmPackageJson, resolvedVersion) + ) { + resolvedVersion = undefined; + } + + if (resolvedVersion) { + targetVersion = resolvedVersion as VersionRange; + } else { + targetVersion = (await getSatisfyingVersion( + registryClient, + npmPackageJson, + distTags[targetVersion] || targetVersion === 'next' ? '*' : targetVersion, + )) as VersionRange | undefined; + } + } + + if (targetVersion && semver.lte(targetVersion, installedVersion)) { + logger.debug(`Package ${name} already satisfied by package.json (${packageJsonRange}).`); + targetVersion = undefined; + } + + let target: PackageVersionInfo | undefined; + if (targetVersion) { + const targetPackageJson = await registryClient.getManifest(name, targetVersion); + if (targetPackageJson) { + target = { + version: targetVersion, + packageJson: targetPackageJson, + updateMetadata: _getUpdateMetadata(targetPackageJson, logger), + }; + } + } + + return { + name, + npmPackageJson, + installed: { + version: installedVersion as VersionRange, + packageJson: installedPackageJson, + updateMetadata: _getUpdateMetadata(installedPackageJson, logger), + }, + target, + packageJsonRange, + }; +} + +function splitPackageName(pkg: string): { name: string; version?: string } { + let name = pkg; + let version: string | undefined; + + if (pkg.startsWith('@')) { + const parts = pkg.split('@'); + name = '@' + parts[1]; + version = parts[2]; + } else if (pkg.includes('@')) { + const parts = pkg.split('@'); + name = parts[0]; + version = parts[1]; + } + + return { name, version }; +} + +function _buildPackageList( + options: UpdateResolverOptions, + allDependencies: ReadonlyMap, + logger: logging.LoggerApi, +): Map { + const packages = new Map(); + const inputPackages = options.packages ?? []; + + if (inputPackages.length === 0) { + return packages; + } + + for (const pkg of inputPackages) { + const { name: pkgName, version: pkgVersion } = splitPackageName(pkg); + + if (!allDependencies.has(pkgName)) { + throw new Error(`Package ${JSON.stringify(pkgName)} is not in package.json.`); + } + + let targetVersion = pkgVersion; + if (options.migrateOnly && !targetVersion && options.from) { + targetVersion = options.from; + } + + packages.set(pkgName, (targetVersion || (options.next ? 'next' : 'latest')) as VersionRange); + } + + return packages; +} + +async function resolvePackageVersion( + registryClient: RegistryClient, + metadata: PackageMetadata, + range: string, + next = false, +): Promise { + const distTags = metadata['dist-tags'] ?? {}; + let resolvedVersion: string | undefined = + distTags[range] ?? (range === 'next' ? distTags['latest'] : undefined); + + if (resolvedVersion && !isReleaseAgeSatisfied(registryClient, metadata, resolvedVersion)) { + resolvedVersion = undefined; + } + + if (resolvedVersion) { + return resolvedVersion; + } + + return getSatisfyingVersion( + registryClient, + metadata, + distTags[range] || range === 'next' ? '*' : range, + next, + ); +} + +async function _addPackageGroup( + packages: Map, + allDependencies: ReadonlyMap, + metadata: PackageMetadata, + registryClient: RegistryClient, + logger: logging.LoggerApi, +): Promise { + const maybePackage = packages.get(metadata.name); + if (!maybePackage) { + return; + } + + const distTags = metadata['dist-tags'] ?? {}; + let version = maybePackage; + let resolvedVersion: string | undefined = + distTags[version] ?? (version === 'next' ? distTags['latest'] : undefined); + + if (resolvedVersion && !isReleaseAgeSatisfied(registryClient, metadata, resolvedVersion)) { + resolvedVersion = undefined; + } + + if (resolvedVersion) { + version = resolvedVersion as VersionRange; + } else { + version = + ((await getSatisfyingVersion( + registryClient, + metadata, + distTags[version] || version === 'next' ? '*' : version, + )) as VersionRange | null) ?? version; + } + + const packageJson = await registryClient.getManifest(metadata.name, version); + if (!packageJson) { + return; + } + const ngUpdateMetadata = packageJson['ng-update']; + if (!ngUpdateMetadata) { + return; + } + + const packageGroup = ngUpdateMetadata['packageGroup']; + if (!packageGroup) { + return; + } + let packageGroupNormalized: Record; + if (Array.isArray(packageGroup) && !packageGroup.some((x) => typeof x != 'string')) { + packageGroupNormalized = packageGroup.reduce( + (acc, curr) => { + acc[curr] = version; + + return acc; + }, + {} as Record, + ); + } else if (typeof packageGroup === 'object' && packageGroup !== null) { + packageGroupNormalized = Object.entries(packageGroup).reduce( + (acc, [name, v]) => { + if (typeof v === 'string') { + acc[name] = v; + } + + return acc; + }, + {} as Record, + ); + } else { + logger.warn(`PackageGroup metadata for ${metadata.name} is malformed. Ignoring.`); + + return; + } + + for (const [member, memberVersion] of Object.entries(packageGroupNormalized)) { + if (packages.has(member)) { + continue; + } + if (allDependencies.has(member)) { + packages.set(member, memberVersion as VersionRange); + } + } +} + +async function _addPeerDependencies( + packages: Map, + allDependencies: ReadonlyMap, + npmPackageJson: PackageMetadata, + workspaceRoot: string, + registryClient: RegistryClient, + logger: logging.LoggerApi, +): Promise { + const maybePackage = packages.get(npmPackageJson.name); + if (!maybePackage) { + return; + } + + const distTags = npmPackageJson['dist-tags'] ?? {}; + const version = distTags[maybePackage] || maybePackage; + const packageJson = await registryClient.getManifest(npmPackageJson.name, version); + if (!packageJson) { + return; + } + + for (const [peer, range] of Object.entries(packageJson.peerDependencies || {})) { + if (packages.has(peer)) { + continue; + } + + const installedVersion = getInstalledVersion(peer, workspaceRoot); + if (installedVersion) { + if (semver.satisfies(installedVersion, range)) { + continue; + } + } else { + const packageJsonRange = allDependencies.get(peer); + if (packageJsonRange) { + const peerMetadata = await registryClient.getMetadata(peer); + if (peerMetadata) { + const resolvedInstalledVersion = await getSatisfyingVersion( + registryClient, + peerMetadata, + packageJsonRange, + ); + + if (resolvedInstalledVersion && semver.satisfies(resolvedInstalledVersion, range)) { + continue; + } + } + } + } + + packages.set(peer, range as VersionRange); + } +} + +function _formatVersion(v?: string): string | undefined { + if (v === undefined) { + return v; + } + if (semver.valid(v)) { + return v; + } + const coerced = semver.coerce(v); + + return coerced ? coerced.toString() : undefined; +} + +function getRegistryNameAndRange(name: string, specifier: string): { name: string; range: string } { + try { + const result = npa.resolve(name, specifier); + if (result.type === 'alias' && result.subSpec) { + return { + name: result.subSpec.name ?? name, + range: result.subSpec.fetchSpec ?? specifier, + }; + } + } catch {} + + return { name, range: specifier }; +} + +function isPkgFromRegistry(name: string, specifier: string): boolean { + const result = npa.resolve(name, specifier); + + return !!result.registry; +} + +async function checkCatalogUpdates( + normalizedPackages: string[], + packageJsonContent: PackageManifest, + registryClient: RegistryClient, + workspaceRoot: string, + options: UpdateResolverOptions, +): Promise { + const catalogUpdates: { name: string; current: string; target: string; specifier: string }[] = []; + + for (const requestedPkg of normalizedPackages) { + const { name: pkgName } = splitPackageName(requestedPkg); + const specifier = + packageJsonContent.dependencies?.[pkgName] || + packageJsonContent.devDependencies?.[pkgName] || + packageJsonContent.peerDependencies?.[pkgName]; + + if (specifier?.startsWith('catalog:')) { + const current = getInstalledVersion(pkgName, workspaceRoot) ?? 'unknown'; + let target = 'latest'; + try { + const metadata = await registryClient.getMetadata(pkgName); + if (metadata) { + const resolved = await resolvePackageVersion( + registryClient, + metadata, + options.next ? 'next' : 'latest', + !!options.next, + ); + target = resolved ?? 'latest'; + } + } catch { + // Fallback to 'latest' tag + } + + catalogUpdates.push({ name: pkgName, current, target, specifier }); + } + } + + if (catalogUpdates.length > 0) { + const packageManagerName = options.packageManager ?? 'your package manager'; + const installCmd = packageManagerName === 'yarn' ? 'yarn install' : 'pnpm install'; + + const updatesList = catalogUpdates + .map((pkg) => ` - ${pkg.name} (${pkg.specifier}) -> Target version: ${pkg.target}`) + .join('\n'); + + const migrationCommands = catalogUpdates + .map((pkg) => { + const fromVer = pkg.current === 'unknown' ? '' : pkg.current; + + return ` ng update ${pkg.name} --migrate-only --from ${fromVer}`; + }) + .join('\n'); + + throw new Error( + `The following packages to update are configured to use \`catalog:\`:\n` + + `${updatesList}\n\n` + + `Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly.\n` + + `Please perform the following steps to update:\n` + + ` 1. Manually update the versions for these packages in your catalog configuration file ` + + `(e.g., pnpm-workspace.yaml or .yarnrc.yml).\n` + + ` 2. Run '${installCmd}' to install the updated versions.\n` + + ` 3. Run the following command(s) from the workspace root to execute the migration schematics:\n` + + `${migrationCommands}`, + ); + } +} + +export async function resolveUserUpdatePlan( + options: UpdateResolverOptions, + packageManager: PackageManager, + logger: logging.LoggerApi, +): Promise { + const workspaceRoot = options.workspaceRoot ?? process.cwd(); + const packageJsonPath = path.join(workspaceRoot, 'package.json'); + if (!existsSync(packageJsonPath)) { + throw new Error('Could not find a package.json. Are you in a Node project?'); + } + + const rawJson = readFileSync(packageJsonPath, 'utf8'); + const packageJsonContent = JSON.parse(rawJson) as PackageManifest; + + const getDependencies = (deps: Record | undefined) => + Object.entries(deps ?? {}).map(([name, range]) => [name, range] as const); + + const allRawDeps = [ + ...getDependencies(packageJsonContent.dependencies), + ...getDependencies(packageJsonContent.devDependencies), + ...getDependencies(packageJsonContent.peerDependencies), + ]; + + const npmDeps = new Map( + allRawDeps.filter(([name, specifier]) => { + try { + return isPkgFromRegistry(name, specifier); + } catch { + logger.warn(`Package ${name} was not found on the registry. Skipping.`); + + return false; + } + }) as [string, VersionRange][], + ); + + const packagesOption = options.packages ?? []; + const normalizedPackages = packagesOption.reduce((acc, curr) => { + return acc.concat(curr.split(',')); + }, [] as string[]); + options.packages = normalizedPackages; + + if (options.migrateOnly && options.from) { + if (options.packages.length !== 1) { + throw new Error('--from requires that only a single package be passed.'); + } + } + + options.from = _formatVersion(options.from); + options.to = _formatVersion(options.to); + const usingYarn = options.packageManager === 'yarn'; + + const minReleaseAge = await packageManager.getMinimumReleaseAge(); + + const getRegistryName = (name: string): string => { + const specifier = npmDeps.get(name); + if (specifier) { + return getRegistryNameAndRange(name, specifier).name; + } + + return name; + }; + + const registryClient = new RegistryClient(packageManager, logger, minReleaseAge, getRegistryName); + + await checkCatalogUpdates( + normalizedPackages, + packageJsonContent, + registryClient, + workspaceRoot, + options, + ); + + const packages = _buildPackageList(options, npmDeps, logger); + + const getOrFetchPackageMetadata = async ( + packageName: string, + ): Promise => { + return registryClient.getMetadata(packageName); + }; + + if (packages.size === 0) { + await Promise.all(Array.from(npmDeps.keys(), (depName) => getOrFetchPackageMetadata(depName))); + } else { + let lastPackagesSize; + do { + lastPackagesSize = packages.size; + + let lastGroupSize; + do { + lastGroupSize = packages.size; + for (const name of Array.from(packages.keys())) { + const metadata = await getOrFetchPackageMetadata(name); + const spec = packages.get(name); + if (metadata && spec) { + const resolvedVersion = await resolvePackageVersion( + registryClient, + metadata, + spec, + !!options.next, + ); + if (resolvedVersion) { + packages.set(name, resolvedVersion as VersionRange); + } + await _addPackageGroup(packages, npmDeps, metadata, registryClient, logger); + } + } + } while (packages.size > lastGroupSize); + + for (const name of Array.from(packages.keys())) { + const metadata = await getOrFetchPackageMetadata(name); + const spec = packages.get(name); + if (metadata && spec) { + const resolvedVersion = await resolvePackageVersion( + registryClient, + metadata, + spec, + !!options.next, + ); + if (resolvedVersion) { + packages.set(name, resolvedVersion as VersionRange); + } + await _addPeerDependencies( + packages, + npmDeps, + metadata, + workspaceRoot, + registryClient, + logger, + ); + } + } + } while (packages.size > lastPackagesSize); + } + + const isListingUpdates = packages.size === 0; + const packageInfoEntries = await Promise.all( + Array.from(npmDeps.keys(), async (depName) => { + const isUpdating = packages.has(depName); + const localPkgJson = getInstalledPackageJson(depName, workspaceRoot); + + if (isListingUpdates || isUpdating || !localPkgJson) { + const metadata = await getOrFetchPackageMetadata(depName); + if (metadata) { + const info = await _buildPackageInfo( + packages, + npmDeps, + metadata, + workspaceRoot, + registryClient, + logger, + ); + + return [depName, info] as const; + } + } + + return [depName, _buildLocalPackageInfo(depName, npmDeps, workspaceRoot)] as const; + }), + ); + const packageInfoMap = new Map(packageInfoEntries); + + const packagesToUpdate = new Map(); + const migrationsToRun: { package: string; collection: string; from: string; to: string }[] = []; + + if (packages.size > 0) { + if (!(options.migrateOnly && options.from && options.packages)) { + const sublog = new logging.LevelCapLogger('validation', logger.createChild(''), 'warn'); + _validateUpdatePackages(packageInfoMap, !!options.force, !!options.next, sublog); + + for (const [name, info] of packageInfoMap.entries()) { + if (!info.target || !info.installed) { + continue; + } + packagesToUpdate.set(name, info.target.version); + + if (info.target.updateMetadata.migrations) { + migrationsToRun.push({ + package: name, + collection: info.target.updateMetadata.migrations, + from: info.installed.version, + to: info.target.version, + }); + } + } + } + } + + return { + packagesToUpdate, + migrationsToRun, + packageInfoMap, + registryClient, + }; +} + +export async function printUpdateUsageMessage( + infoMap: Map, + registryClient: RegistryClient, + logger: logging.LoggerApi, + next = false, +): Promise { + const packageGroups = new Map(); + const mappedPackages = await Promise.all( + Array.from(infoMap.entries(), async ([name, info]) => { + const distTags = info.npmPackageJson['dist-tags'] ?? {}; + let tag = next ? (distTags['next'] ? 'next' : 'latest') : 'latest'; + let version = distTags[tag] ?? info.installed.version; + const versions = info.npmPackageJson.versions ?? []; + + const versionDiff = semver.diff(info.installed.version, version); + if ( + versionDiff !== 'patch' && + versionDiff !== 'minor' && + /^@(?:angular|nguniversal)\//.test(name) + ) { + const installedMajorVersion = semver.parse(info.installed.version)?.major; + const toInstallMajorVersion = semver.parse(version)?.major; + if ( + installedMajorVersion !== undefined && + toInstallMajorVersion !== undefined && + installedMajorVersion < toInstallMajorVersion - 1 + ) { + const nextMajorVersion = `${installedMajorVersion + 1}.`; + const nextMajorVersions = versions + .filter((v) => v.startsWith(nextMajorVersion)) + .sort((a, b) => (a > b ? -1 : 1)); + + if (nextMajorVersions.length) { + version = nextMajorVersions[0]; + tag = ''; + } + } + } + + const target = info.target?.packageJson || (await registryClient.getManifest(name, version)); + + return { + name, + info, + version, + tag, + target, + }; + }), + ); + + const packagesToUpdate = mappedPackages + .filter( + ({ info, version, target }) => + target?.['ng-update'] && semver.compare(info.installed.version, version) < 0, + ) + .map(({ name, info, version, tag, target }) => { + // Look for packageGroup. + const ngUpdate = target?.['ng-update'] as Record | undefined; + const packageGroup = ngUpdate?.['packageGroup']; + if (packageGroup) { + const packageGroupNames = Array.isArray(packageGroup) + ? packageGroup + : Object.keys(packageGroup); + const packageGroupName = + (ngUpdate?.['packageGroupName'] as string | undefined) || + packageGroupNames.find((n) => infoMap.has(n)); + + if (packageGroupName) { + if (packageGroups.has(name)) { + return null; + } + + for (const groupName of packageGroupNames) { + packageGroups.set(groupName, packageGroupName); + } + + packageGroups.set(packageGroupName, packageGroupName); + name = packageGroupName; + } + } + + let command = `ng update ${name}`; + if (!tag) { + command += `@${semver.parse(version)?.major || version}`; + } else if (tag == 'next') { + command += ' --next'; + } + + return [name, `${info.installed.version} -> ${version} `, command]; + }) + .filter((x): x is string[] => x !== null) + .sort((a, b) => a[0].localeCompare(b[0])); + + if (packagesToUpdate.length == 0) { + logger.info('We analyzed your package.json and everything seems to be in order. Good work!'); + + return; + } + + logger.info('We analyzed your package.json, there are some packages to update:\n'); + + // Find the largest name to know the padding needed. + let namePad = Math.max(...[...infoMap.keys()].map((x) => x.length)) + 2; + if (!Number.isFinite(namePad)) { + namePad = 30; + } + const pads = [namePad, 25, 0]; + + logger.info( + ' ' + ['Name', 'Version', 'Command to update'].map((x, i) => x.padEnd(pads[i])).join(''), + ); + + const totalWidth = pads.reduce((sum, width) => sum + width, 20); + logger.info(` ${'-'.repeat(totalWidth)}`); + + packagesToUpdate.forEach((fields) => { + if (!fields) { + return; + } + + logger.info(' ' + fields.map((x, i) => x.padEnd(pads[i])).join('')); + }); + + logger.info( + `\nThere might be additional packages which don't provide 'ng update' capabilities that are outdated.\n` + + `You can update the additional packages by running the update command of your package manager.`, + ); +} + +export async function applyUpdatePlan( + workspaceRoot: string, + plan: UpdatePlan, + logger: logging.LoggerApi, +): Promise { + const packageJsonPath = path.join(workspaceRoot, 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonContent) as PackageManifest; + + const updateDependency = (deps: Record, name: string, newVersion: string) => { + const oldVersion = deps[name]; + const aliasPrefix = 'npm:'; + + // If the dependency uses an npm package alias (e.g., "npm:registry-name@version-range"), + // parse and reconstruct the alias with the new target version while preserving + // the original alias registry name and any version prefix character (like ^ or ~). + if (oldVersion.startsWith(aliasPrefix)) { + const specifier = oldVersion.slice(aliasPrefix.length); + const lastAtIndex = specifier.lastIndexOf('@'); + if (lastAtIndex > 0) { + const registryName = specifier.slice(0, lastAtIndex); + const versionRange = specifier.slice(lastAtIndex + 1); + // Retain any semantic versioning operator prefix (e.g., ^ or ~) from the target version range. + const execResult = /^[\^~]/.exec(versionRange); + deps[name] = + `${aliasPrefix}${registryName}@${execResult ? execResult[0] : ''}${newVersion}`; + } else { + // If there's no `@` character defining a version specifier in the alias (e.g. "npm:packageName"), + // leave it as-is without attempting to inject a version suffix. + deps[name] = oldVersion; + } + } else { + // Standard dependency formatting, keeping any semantic versioning operator prefix (e.g., ^ or ~). + const execResult = /^[\^~]/.exec(oldVersion); + deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`; + } + }; + + for (const [name, targetVersion] of plan.packagesToUpdate.entries()) { + logger.info(`Updating package.json with dependency ${name} to version ${targetVersion}...`); + + if (packageJson.dependencies && packageJson.dependencies[name]) { + updateDependency(packageJson.dependencies, name, targetVersion); + if (packageJson.devDependencies) { + delete packageJson.devDependencies[name]; + } + if (packageJson.peerDependencies) { + delete packageJson.peerDependencies[name]; + } + } else if (packageJson.devDependencies && packageJson.devDependencies[name]) { + updateDependency(packageJson.devDependencies, name, targetVersion); + if (packageJson.peerDependencies) { + delete packageJson.peerDependencies[name]; + } + } else if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + updateDependency(packageJson.peerDependencies, name, targetVersion); + } else { + if (!packageJson.dependencies) { + packageJson.dependencies = {}; + } + packageJson.dependencies[name] = `^${targetVersion}`; + } + } + + const eofMatches = packageJsonContent.match(/\r?\n$/); + const eof = eofMatches?.[0] ?? ''; + const newContent = JSON.stringify(packageJson, null, 2) + eof; + await fs.writeFile(packageJsonPath, newContent, 'utf8'); +} diff --git a/packages/angular/cli/src/commands/update/update-resolver_spec.ts b/packages/angular/cli/src/commands/update/update-resolver_spec.ts new file mode 100644 index 000000000000..9ced5829587e --- /dev/null +++ b/packages/angular/cli/src/commands/update/update-resolver_spec.ts @@ -0,0 +1,764 @@ +/** + * @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 { logging } from '@angular-devkit/core'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import * as semver from 'semver'; +import type { PackageManager, PackageManifest, PackageMetadata } from '../../package-managers'; +import { + RegistryClient, + UpdateResolverOptions, + angularMajorCompatGuarantee, + applyUpdatePlan, + resolveUserUpdatePlan, +} from './update-resolver'; + +describe('angularMajorCompatGuarantee', () => { + [ + '5.0.0', + '5.1.0', + '5.20.0', + '6.0.0', + '6.0.0-rc.0', + '6.0.0-beta.0', + '6.1.0-beta.0', + '6.1.0-rc.0', + '6.10.11', + ].forEach((golden) => { + it('works with ' + JSON.stringify(golden), () => { + expect(semver.satisfies(golden, angularMajorCompatGuarantee('^5.0.0'))).toBeTruthy(); + }); + }); +}); + +describe('UpdateResolver', () => { + let tempRoot: string; + const logger = new logging.NullLogger(); + + beforeEach(() => { + tempRoot = mkdtempSync(path.join(tmpdir(), 'angular-cli-update-resolver-test-')); + }); + + afterEach(() => { + rmSync(tempRoot, { recursive: true, force: true }); + }); + + const MOCK_REGISTRY: Record< + string, + { + metadata: PackageMetadata; + manifests: Record; + } + > = { + '@angular-devkit-tests/update-base': { + metadata: { + name: '@angular-devkit-tests/update-base', + 'dist-tags': { latest: '1.1.0' }, + versions: ['1.0.0', '1.1.0'], + }, + manifests: { + '1.0.0': { name: '@angular-devkit-tests/update-base', version: '1.0.0' }, + '1.1.0': { name: '@angular-devkit-tests/update-base', version: '1.1.0' }, + }, + }, + '@angular-devkit-tests/update-peer-dependencies-angular-5': { + metadata: { + name: '@angular-devkit-tests/update-peer-dependencies-angular-5', + 'dist-tags': { latest: '1.0.0' }, + versions: ['1.0.0'], + }, + manifests: { + '1.0.0': { + name: '@angular-devkit-tests/update-peer-dependencies-angular-5', + version: '1.0.0', + peerDependencies: { + '@angular/core': '^5.0.0', + }, + }, + }, + }, + '@angular-devkit-tests/update-package-group-1': { + metadata: { + name: '@angular-devkit-tests/update-package-group-1', + 'dist-tags': { latest: '1.2.0' }, + versions: ['1.0.0', '1.2.0'], + }, + manifests: { + '1.0.0': { name: '@angular-devkit-tests/update-package-group-1', version: '1.0.0' }, + '1.2.0': { + name: '@angular-devkit-tests/update-package-group-1', + version: '1.2.0', + 'ng-update': { + packageGroup: { + '@angular-devkit-tests/update-package-group-1': '', + '@angular-devkit-tests/update-package-group-2': '^2', + }, + }, + }, + }, + }, + '@angular-devkit-tests/update-package-group-2': { + metadata: { + name: '@angular-devkit-tests/update-package-group-2', + 'dist-tags': { latest: '2.0.0' }, + versions: ['1.0.0', '2.0.0'], + }, + manifests: { + '1.0.0': { name: '@angular-devkit-tests/update-package-group-2', version: '1.0.0' }, + '2.0.0': { + name: '@angular-devkit-tests/update-package-group-2', + version: '2.0.0', + 'ng-update': { + packageGroup: { + '@angular-devkit-tests/update-package-group-1': '^1', + '@angular-devkit-tests/update-package-group-2': '', + }, + }, + }, + }, + }, + '@angular/core': { + metadata: { + name: '@angular/core', + 'dist-tags': { latest: '6.0.0' }, + versions: ['5.1.0', '6.0.0'], + }, + manifests: { + '5.1.0': { name: '@angular/core', version: '5.1.0' }, + '6.0.0': { name: '@angular/core', version: '6.0.0' }, + }, + }, + 'rxjs': { + metadata: { + name: 'rxjs', + 'dist-tags': { latest: '5.5.0' }, + versions: ['5.5.0'], + }, + manifests: { + '5.5.0': { name: 'rxjs', version: '5.5.0' }, + }, + }, + 'zone.js': { + metadata: { + name: 'zone.js', + 'dist-tags': { latest: '0.8.26' }, + versions: ['0.8.26'], + }, + manifests: { + '0.8.26': { name: 'zone.js', version: '0.8.26' }, + }, + }, + '@angular-devkit-tests/update-release-age': { + metadata: { + name: '@angular-devkit-tests/update-release-age', + 'dist-tags': { latest: '1.2.0' }, + versions: ['1.0.0', '1.1.0', '1.2.0'], + time: { + '1.0.0': '2026-06-01T00:00:00.000Z', + '1.1.0': new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), + '1.2.0': new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), + }, + }, + manifests: { + '1.0.0': { name: '@angular-devkit-tests/update-release-age', version: '1.0.0' }, + '1.1.0': { name: '@angular-devkit-tests/update-release-age', version: '1.1.0' }, + '1.2.0': { name: '@angular-devkit-tests/update-release-age', version: '1.2.0' }, + }, + }, + '@angular-devkit-tests/core-bug': { + metadata: { + name: '@angular-devkit-tests/core-bug', + 'dist-tags': { latest: '14.0.0-next.0' }, + versions: ['12.2.0', '13.2.0', '14.0.0-next.0'], + }, + manifests: { + '12.2.0': { name: '@angular-devkit-tests/core-bug', version: '12.2.0' }, + '13.2.0': { + name: '@angular-devkit-tests/core-bug', + version: '13.2.0', + 'ng-update': { + packageGroup: ['@angular-devkit-tests/core-bug', '@angular-devkit-tests/common-bug'], + }, + }, + '14.0.0-next.0': { name: '@angular-devkit-tests/core-bug', version: '14.0.0-next.0' }, + }, + }, + '@angular-devkit-tests/common-bug': { + metadata: { + name: '@angular-devkit-tests/common-bug', + 'dist-tags': { latest: '14.0.0-next.0' }, + versions: ['12.2.0', '13.2.0', '14.0.0-next.0'], + }, + manifests: { + '12.2.0': { name: '@angular-devkit-tests/common-bug', version: '12.2.0' }, + '13.2.0': { name: '@angular-devkit-tests/common-bug', version: '13.2.0' }, + '14.0.0-next.0': { name: '@angular-devkit-tests/common-bug', version: '14.0.0-next.0' }, + }, + }, + '@angular-devkit-tests/cdk-bug': { + metadata: { + name: '@angular-devkit-tests/cdk-bug', + 'dist-tags': { latest: '14.0.0-next.0' }, + versions: ['12.2.0', '13.2.0', '14.0.0-next.0'], + }, + manifests: { + '12.2.0': { + name: '@angular-devkit-tests/cdk-bug', + version: '12.2.0', + peerDependencies: { + '@angular-devkit-tests/core-bug': '^12.0.0', + '@angular-devkit-tests/common-bug': '^12.0.0', + }, + }, + '13.2.0': { + name: '@angular-devkit-tests/cdk-bug', + version: '13.2.0', + peerDependencies: { + '@angular-devkit-tests/core-bug': '^13.0.0 || ^14.0.0-0', + '@angular-devkit-tests/common-bug': '^13.0.0 || ^14.0.0-0', + }, + }, + '14.0.0-next.0': { name: '@angular-devkit-tests/cdk-bug', version: '14.0.0-next.0' }, + }, + }, + '@types/web': { + metadata: { + name: '@types/web', + 'dist-tags': { latest: '0.0.100' }, + versions: ['0.0.99', '0.0.100'], + }, + manifests: { + '0.0.99': { name: '@types/web', version: '0.0.99' }, + '0.0.100': { name: '@types/web', version: '0.0.100' }, + }, + }, + }; + + async function resolvePlan(options: UpdateResolverOptions, minReleaseAge = 0) { + const mockPackageManager = { + name: 'npm', + async getRegistryMetadata(packageName: string) { + return MOCK_REGISTRY[packageName]?.metadata ?? null; + }, + async getRegistryManifest(packageName: string, version: string) { + return MOCK_REGISTRY[packageName]?.manifests[version] ?? null; + }, + async getMinimumReleaseAge() { + return minReleaseAge; + }, + } as unknown as PackageManager; + + return resolveUserUpdatePlan(options, mockPackageManager, logger); + } + + function createMockWorkspace( + packageJson: Record, + nodeModules: { [name: string]: { version: string; manifest?: Record } } = {}, + ) { + writeFileSync(path.join(tempRoot, 'package.json'), JSON.stringify(packageJson, null, 2)); + for (const [name, info] of Object.entries(nodeModules)) { + const pkgDir = path.join(tempRoot, 'node_modules', name); + mkdirSync(pkgDir, { recursive: true }); + writeFileSync( + path.join(pkgDir, 'package.json'), + JSON.stringify({ name, version: info.version, ...info.manifest }, null, 2), + ); + } + } + + it('ignores dependencies not hosted on the NPM registry', async () => { + createMockWorkspace({ + name: 'blah', + dependencies: { + '@angular-devkit-tests/update-base': 'file:update-base-1.0.0.tgz', + }, + }); + + const plan = await resolvePlan({ + packages: [], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.size).toBe(0); + }); + + it('includes registry metadata when listing updates with locked dependency versions in package.json', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular-devkit-tests/update-base': '1.0.0', + }, + }, + { + '@angular-devkit-tests/update-base': { version: '1.0.0' }, + }, + ); + + const plan = await resolvePlan({ + packages: [], + workspaceRoot: tempRoot, + }); + + const info = plan.packageInfoMap.get('@angular-devkit-tests/update-base'); + expect(info?.npmPackageJson['dist-tags']?.['latest']).toBe('1.1.0'); + expect(info?.npmPackageJson.versions).toContain('1.1.0'); + }); + + it('should not error with yarn 2.0 protocols', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + src: 'src@link:./src', + '@angular-devkit-tests/update-base': '1.0.0', + }, + }, + { + '@angular-devkit-tests/update-base': { version: '1.0.0' }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@angular-devkit-tests/update-base'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@angular-devkit-tests/update-base')).toBe('1.1.0'); + }); + + it('updates Angular as compatible with Angular N-1', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular-devkit-tests/update-peer-dependencies-angular-5': '1.0.0', + '@angular/core': '5.1.0', + rxjs: '5.5.0', + 'zone.js': '0.8.26', + }, + }, + { + '@angular-devkit-tests/update-peer-dependencies-angular-5': { version: '1.0.0' }, + '@angular/core': { version: '5.1.0' }, + rxjs: { version: '5.5.0' }, + 'zone.js': { version: '0.8.26' }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@angular/core@^6.0.0'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@angular/core')?.[0]).toBe('6'); + }); + + it('uses packageGroup for versioning', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular-devkit-tests/update-package-group-1': '1.0.0', + '@angular-devkit-tests/update-package-group-2': '1.0.0', + }, + }, + { + '@angular-devkit-tests/update-package-group-1': { version: '1.0.0' }, + '@angular-devkit-tests/update-package-group-2': { version: '1.0.0' }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@angular-devkit-tests/update-package-group-1'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@angular-devkit-tests/update-package-group-1')).toBe('1.2.0'); + expect(plan.packagesToUpdate.get('@angular-devkit-tests/update-package-group-2')).toBe('2.0.0'); + }); + + it('does not remove newline at the end of package.json', async () => { + const newline = '\n'; + const packageJsonContent = `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "1.0.0" + } +}${newline}`; + + writeFileSync(path.join(tempRoot, 'package.json'), packageJsonContent); + + // Mock installed package + const pkgDir = path.join(tempRoot, 'node_modules', '@angular-devkit-tests/update-base'); + mkdirSync(pkgDir, { recursive: true }); + writeFileSync( + path.join(pkgDir, 'package.json'), + JSON.stringify({ name: '@angular-devkit-tests/update-base', version: '1.0.0' }, null, 2), + ); + + const plan = await resolvePlan({ + packages: ['@angular-devkit-tests/update-base'], + workspaceRoot: tempRoot, + }); + + await applyUpdatePlan(tempRoot, plan, logger); + + const result = readFileSync(path.join(tempRoot, 'package.json'), 'utf8'); + expect(result.endsWith(newline)).toBeTrue(); + }); + + it('does not add a newline at the end of package.json', async () => { + const packageJsonContent = `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "1.0.0" + } +}`; + + writeFileSync(path.join(tempRoot, 'package.json'), packageJsonContent); + + // Mock installed package + const pkgDir = path.join(tempRoot, 'node_modules', '@angular-devkit-tests/update-base'); + mkdirSync(pkgDir, { recursive: true }); + writeFileSync( + path.join(pkgDir, 'package.json'), + JSON.stringify({ name: '@angular-devkit-tests/update-base', version: '1.0.0' }, null, 2), + ); + + const plan = await resolvePlan({ + packages: ['@angular-devkit-tests/update-base'], + workspaceRoot: tempRoot, + }); + + await applyUpdatePlan(tempRoot, plan, logger); + + const result = readFileSync(path.join(tempRoot, 'package.json'), 'utf8'); + expect(result.endsWith('}')).toBeTrue(); + }); + + it('respects minimumReleaseAge and filters out versions published too recently', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular-devkit-tests/update-release-age': '1.0.0', + }, + }, + { + '@angular-devkit-tests/update-release-age': { version: '1.0.0' }, + }, + ); + + const planNoFilter = await resolvePlan( + { + packages: ['@angular-devkit-tests/update-release-age'], + workspaceRoot: tempRoot, + }, + 0, + ); + + expect(planNoFilter.packagesToUpdate.get('@angular-devkit-tests/update-release-age')).toBe( + '1.2.0', + ); + + const planWithFilter = await resolvePlan( + { + packages: ['@angular-devkit-tests/update-release-age'], + workspaceRoot: tempRoot, + }, + 3 * 24 * 60 * 60 * 1000, + ); + + expect(planWithFilter.packagesToUpdate.get('@angular-devkit-tests/update-release-age')).toBe( + '1.1.0', + ); + }); + + it('correctly keeps packageGroup version when updating alongside peer dependencies', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular-devkit-tests/core-bug': '12.2.0', + '@angular-devkit-tests/common-bug': '12.2.0', + '@angular-devkit-tests/cdk-bug': '12.2.0', + }, + }, + { + '@angular-devkit-tests/core-bug': { version: '12.2.0' }, + '@angular-devkit-tests/common-bug': { version: '12.2.0' }, + '@angular-devkit-tests/cdk-bug': { version: '12.2.0' }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@angular-devkit-tests/cdk-bug@13', '@angular-devkit-tests/core-bug@13'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@angular-devkit-tests/core-bug')).toBe('13.2.0'); + expect(plan.packagesToUpdate.get('@angular-devkit-tests/common-bug')).toBe('13.2.0'); + expect(plan.packagesToUpdate.get('@angular-devkit-tests/cdk-bug')).toBe('13.2.0'); + }); + + it('rejects updates for catalog packages and guides on manual migration steps', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular/core': 'catalog:', + }, + }, + { + '@angular/core': { version: '5.1.0' }, + }, + ); + + const expectedError = ` +The following packages to update are configured to use \`catalog:\`: + - @angular/core \\(catalog:\\) -> Target version: 6\\.0\\.0 + +Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly\\. +Please perform the following steps to update: + 1\\. Manually update the versions for these packages in your catalog.* + 2\\. Run 'pnpm install' to install the updated versions\\. + 3\\. Run the following command\\(s\\) from the workspace root to execute the migration schematics: + ng update @angular/core --migrate-only --from 5\\.1\\.0 + `.trim(); + + await expectAsync( + resolvePlan({ + packages: ['@angular/core'], + workspaceRoot: tempRoot, + }), + ).toBeRejectedWithError(new RegExp(expectedError)); + }); + + it('rejects updates for catalog packages specified with a version suffix', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular/core': 'catalog:', + }, + }, + { + '@angular/core': { version: '5.1.0' }, + }, + ); + + const expectedError = ` +The following packages to update are configured to use \`catalog:\`: + - @angular/core \\(catalog:\\) -> Target version: 6\\.0\\.0 + +Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly\\. +Please perform the following steps to update: + 1\\. Manually update the versions for these packages in your catalog.* + 2\\. Run 'pnpm install' to install the updated versions\\. + 3\\. Run the following command\\(s\\) from the workspace root to execute the migration schematics: + ng update @angular/core --migrate-only --from 5\\.1\\.0 + `.trim(); + + await expectAsync( + resolvePlan({ + packages: ['@angular/core@6'], + workspaceRoot: tempRoot, + }), + ).toBeRejectedWithError(new RegExp(expectedError)); + }); + + it('rejects updates for catalog packages specified with a named catalog', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@angular/core': 'catalog:framework', + }, + }, + { + '@angular/core': { version: '5.1.0' }, + }, + ); + + const expectedError = ` +The following packages to update are configured to use \`catalog:\`: + - @angular/core \\(catalog:framework\\) -> Target version: 6\\.0\\.0 + +Because catalogs are shared across the monorepo, 'ng update' cannot modify them directly\\. +Please perform the following steps to update: + 1\\. Manually update the versions for these packages in your catalog.* + 2\\. Run 'pnpm install' to install the updated versions\\. + 3\\. Run the following command\\(s\\) from the workspace root to execute the migration schematics: + ng update @angular/core --migrate-only --from 5\\.1\\.0 + `.trim(); + + await expectAsync( + resolvePlan({ + packages: ['@angular/core'], + workspaceRoot: tempRoot, + }), + ).toBeRejectedWithError(new RegExp(expectedError)); + }); + + it('correctly resolves and updates packages using npm alias syntax', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@typescript/lib-dom': 'npm:@types/web@^0.0.99', + }, + }, + { + '@typescript/lib-dom': { + version: '0.0.99', + manifest: { name: '@types/web' }, + }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@typescript/lib-dom'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100'); + + await applyUpdatePlan(tempRoot, plan, logger); + + const updatedPackageJson = JSON.parse( + readFileSync(path.join(tempRoot, 'package.json'), 'utf8'), + ) as PackageManifest; + + expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe( + 'npm:@types/web@^0.0.100', + ); + }); + + it('correctly handles packages using npm alias syntax without a version range', async () => { + createMockWorkspace( + { + name: 'blah', + dependencies: { + '@typescript/lib-dom': 'npm:@types/web', + }, + }, + { + '@typescript/lib-dom': { + version: '0.0.99', + manifest: { name: '@types/web' }, + }, + }, + ); + + const plan = await resolvePlan({ + packages: ['@typescript/lib-dom'], + workspaceRoot: tempRoot, + }); + + expect(plan.packagesToUpdate.get('@typescript/lib-dom')).toBe('0.0.100'); + + await applyUpdatePlan(tempRoot, plan, logger); + + const updatedPackageJson = JSON.parse( + readFileSync(path.join(tempRoot, 'package.json'), 'utf8'), + ) as PackageManifest; + + expect(updatedPackageJson.dependencies?.['@typescript/lib-dom']).toBe('npm:@types/web'); + }); +}); + +describe('RegistryClient', () => { + const logger = new logging.NullLogger(); + + it('should cache metadata requests', async () => { + let callCount = 0; + const mockPackageManager = { + async getRegistryMetadata() { + callCount++; + + return { name: 'test-pkg', 'dist-tags': {}, versions: [] }; + }, + } as unknown as PackageManager; + + const client = new RegistryClient(mockPackageManager, logger); + const m1 = await client.getMetadata('test-pkg'); + const m2 = await client.getMetadata('test-pkg'); + + expect(callCount).toBe(1); + expect(m1).toEqual(m2); + }); + + it('should evict metadata requests from cache upon failure', async () => { + let callCount = 0; + const mockPackageManager = { + async getRegistryMetadata() { + callCount++; + if (callCount === 1) { + throw new Error('Transient error'); + } + + return { name: 'test-pkg', 'dist-tags': {}, versions: [] }; + }, + } as unknown as PackageManager; + + const client = new RegistryClient(mockPackageManager, logger); + + await expectAsync(client.getMetadata('test-pkg')).toBeRejectedWithError('Transient error'); + const m2 = await client.getMetadata('test-pkg'); + + expect(callCount).toBe(2); + expect(m2).not.toBeNull(); + expect(m2?.name).toBe('test-pkg'); + }); + + it('should cache manifest requests', async () => { + let callCount = 0; + const mockPackageManager = { + async getRegistryManifest() { + callCount++; + + return { name: 'test-pkg', version: '1.0.0' }; + }, + } as unknown as PackageManager; + + const client = new RegistryClient(mockPackageManager, logger); + const m1 = await client.getManifest('test-pkg', '1.0.0'); + const m2 = await client.getManifest('test-pkg', '1.0.0'); + + expect(callCount).toBe(1); + expect(m1).toEqual(m2); + }); + + it('should evict manifest requests from cache upon failure', async () => { + let callCount = 0; + const mockPackageManager = { + async getRegistryManifest() { + callCount++; + if (callCount === 1) { + throw new Error('Transient error'); + } + + return { name: 'test-pkg', version: '1.0.0' }; + }, + } as unknown as PackageManager; + + const client = new RegistryClient(mockPackageManager, logger); + + await expectAsync(client.getManifest('test-pkg', '1.0.0')).toBeRejectedWithError( + 'Transient error', + ); + const m2 = await client.getManifest('test-pkg', '1.0.0'); + + expect(callCount).toBe(2); + expect(m2).not.toBeNull(); + expect(m2?.version).toBe('1.0.0'); + }); +}); diff --git a/packages/angular/cli/src/commands/update/utilities/cli-version.ts b/packages/angular/cli/src/commands/update/utilities/cli-version.ts index 6067b81504d4..3791f7ad7070 100644 --- a/packages/angular/cli/src/commands/update/utilities/cli-version.ts +++ b/packages/angular/cli/src/commands/update/utilities/cli-version.ts @@ -70,7 +70,7 @@ export async function checkCLIVersion( const version = manifest.version; - return VERSION.full === version ? null : version; + return VERSION.full === version ? null : String(runnerVersion); } /** diff --git a/packages/angular/cli/src/commands/update/utilities/git.ts b/packages/angular/cli/src/commands/update/utilities/git.ts index 0998c9c61fff..1714846117d9 100644 --- a/packages/angular/cli/src/commands/update/utilities/git.ts +++ b/packages/angular/cli/src/commands/update/utilities/git.ts @@ -60,7 +60,7 @@ export function checkCleanGit(root: string): boolean { return false; } } - } catch {} // eslint-disable-line no-empty + } catch {} return true; } diff --git a/packages/angular/cli/src/commands/update/utilities/migration.ts b/packages/angular/cli/src/commands/update/utilities/migration.ts index f52726de5523..83a1492eaa10 100644 --- a/packages/angular/cli/src/commands/update/utilities/migration.ts +++ b/packages/angular/cli/src/commands/update/utilities/migration.ts @@ -13,10 +13,11 @@ import { FileSystemSchematicDescription, NodeWorkflow, } from '@angular-devkit/schematics/tools'; +import { figures } from 'listr2'; import { SpawnSyncReturns } from 'node:child_process'; import * as semver from 'semver'; import { subscribeToWorkflow } from '../../../command-builder/utilities/schematic-workflow'; -import { colors, figures } from '../../../utilities/color'; +import { colors } from '../../../utilities/color'; import { assertIsError } from '../../../utilities/error'; import { writeErrorToLogFile } from '../../../utilities/log-file'; import { formatFiles } from '../../../utilities/prettier'; @@ -153,9 +154,7 @@ export async function executeMigrations( if (requiredMigrations.length) { logger.info(colors.cyan(`** Executing migrations of package '${packageName}' **\n`)); - requiredMigrations.sort( - (a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name), - ); + requiredMigrations.sort(compareMigrations); const result = await executePackageMigrations( workflow, @@ -174,9 +173,7 @@ export async function executeMigrations( if (optionalMigrations.length) { logger.info(colors.magenta(`** Optional migrations of package '${packageName}' **\n`)); - optionalMigrations.sort( - (a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name), - ); + optionalMigrations.sort(compareMigrations); const migrationsToRun = await getOptionalMigrationsToRun( logger, @@ -372,3 +369,10 @@ function getMigrationTitleAndDescription(migration: MigrationSchematicDescriptio : undefined, }; } + +function compareMigrations( + a: MigrationSchematicDescriptionWithVersion, + b: MigrationSchematicDescriptionWithVersion, +): number { + return semver.compare(a.version, b.version); +} diff --git a/packages/angular/cli/src/package-managers/host.ts b/packages/angular/cli/src/package-managers/host.ts index cee68015f677..90f426f9ab71 100644 --- a/packages/angular/cli/src/package-managers/host.ts +++ b/packages/angular/cli/src/package-managers/host.ts @@ -129,18 +129,33 @@ export const NodeJS_HOST: Host = { const isWin32 = platform() === 'win32'; return new Promise((resolve, reject) => { + const env: Record = { + ...process.env, + ...options.env, + // NPM updater notifier will prevents the child process from closing until it timeout after 3 minutes. + NO_UPDATE_NOTIFIER: '1', + NPM_CONFIG_UPDATE_NOTIFIER: 'false', + }; + + // When running via Yarn Classic (`yarn run