diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 1cd5702c4943..46d49f7d0238 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - name: 'Setup Rust toolchain' - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: target: ${{ inputs.targets }} # needed to not make it override the defaults diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 7ceb840e63c3..53d674b6bf76 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -47,12 +47,12 @@ jobs: value: ${{ steps.deploy-target.outputs.value }} release_environment: ${{ steps.deploy-target.outputs.release_environment }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - run: echo "${{ github.event.after }}" - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -120,7 +120,7 @@ jobs: NEXT_SKIP_NATIVE_POSTINSTALL: 1 steps: - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -129,14 +129,14 @@ jobs: npm i -g corepack@0.31 corepack enable - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 25 - id: get-store-path run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 timeout-minutes: 5 id: cache-pnpm-store with: @@ -154,7 +154,7 @@ jobs: - run: pnpm run build - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 timeout-minutes: 5 id: cache-build with: @@ -236,14 +236,14 @@ jobs: # we use checkout here instead of the build cache since # it can fail to restore in different OS' - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # crates/next-napi-bindings/build.rs uses git-describe to find the most recent git tag. It's okay if # this fails, but fetch with enough depth that we're likely to find a recent tag. fetch-depth: 100 - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 if: ${{ !matrix.docker }} with: node-version: ${{ env.NODE_LTS_VERSION }} @@ -394,10 +394,10 @@ jobs: runs-on: ubuntu-latest-16-core-oss steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -443,7 +443,7 @@ jobs: - build-native steps: - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -456,7 +456,7 @@ jobs: - name: tune linux network run: sudo ethtool -K eth0 tx off rx off - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 timeout-minutes: 5 id: restore-build with: @@ -507,7 +507,7 @@ jobs: id-token: write steps: - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 check-latest: true @@ -521,7 +521,7 @@ jobs: - name: tune linux network run: sudo ethtool -K eth0 tx off rx off - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 timeout-minutes: 5 id: restore-build with: @@ -547,7 +547,7 @@ jobs: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -578,7 +578,7 @@ jobs: env: DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | .github diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index c275b9f1b1e3..ca9a3220e3da 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -31,7 +31,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 @@ -147,7 +147,7 @@ jobs: if: ${{ needs.changes.outputs.docs-only == 'false' }} steps: - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -158,7 +158,7 @@ jobs: corepack enable - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 @@ -205,8 +205,8 @@ jobs: validate-docs-links: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 - name: Setup corepack @@ -284,7 +284,7 @@ jobs: runs-on: ubuntu-latest name: ast-grep lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: ast-grep lint step uses: ast-grep/action@cf62e780f0c88301228978d593a7784427a097a6 # v1.5.0 with: diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml index abc9e716f337..223e526175f1 100644 --- a/.github/workflows/build_reusable.yml +++ b/.github/workflows/build_reusable.yml @@ -182,7 +182,7 @@ jobs: fnm env --json | jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' | xargs -I {} echo "{}" >> $GITHUB_ENV - name: Normalize input step names into path key - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 id: var with: script: | @@ -224,7 +224,7 @@ jobs: - run: rm -rf .git - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 @@ -304,7 +304,7 @@ jobs: # If keep conditions in sync breaks, we can split into restore and save # steps where saving runs based on the outcome of the install step if: ${{ runner.environment == 'github-hosted' && (inputs.skipInstallBuild != 'yes' || inputs.needsNextest == 'yes') }} - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 timeout-minutes: 5 id: cache-pnpm-store with: diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index d236f68e8bca..20d608f0a741 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -27,7 +27,7 @@ jobs: environment: release-${{ github.event.inputs.releaseType }} steps: - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 check-latest: true diff --git a/.github/workflows/create_release_branch.yml b/.github/workflows/create_release_branch.yml index bf5d6ab12247..b4ddfc4f9686 100644 --- a/.github/workflows/create_release_branch.yml +++ b/.github/workflows/create_release_branch.yml @@ -30,14 +30,14 @@ jobs: steps: - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 check-latest: true - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -76,7 +76,7 @@ jobs: - id: get-store-path run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 timeout-minutes: 5 id: cache-pnpm-store with: diff --git a/.github/workflows/integration_tests_reusable.yml b/.github/workflows/integration_tests_reusable.yml index fe9a654172ce..ab1ee6f4f585 100644 --- a/.github/workflows/integration_tests_reusable.yml +++ b/.github/workflows/integration_tests_reusable.yml @@ -153,7 +153,7 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Collect integration test stat uses: ./.github/actions/next-integration-stat diff --git a/.github/workflows/issue_stale.yml b/.github/workflows/issue_stale.yml index ce09df573b18..962c99c680ce 100644 --- a/.github/workflows/issue_stale.yml +++ b/.github/workflows/issue_stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: github.repository_owner == 'vercel' steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 id: issue-stale name: 'Mark stale issues, close stale issues' with: @@ -26,7 +26,7 @@ jobs: stale-issue-message: 'This issue has been automatically marked as stale due to inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you.' close-issue-message: 'This issue has been automatically closed due to inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!' operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 id: stale-no-repro name: 'Close stale issues with no reproduction' with: @@ -41,7 +41,7 @@ jobs: stale-issue-label: 'stale' labels-to-add-when-unstale: 'not stale' operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 id: stale-simple-repro name: 'Close issues with no simple repro' with: @@ -56,7 +56,7 @@ jobs: stale-issue-label: 'stale' labels-to-add-when-unstale: 'not stale' operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 id: stale-no-canary name: 'Close issues not verified on canary' with: diff --git a/.github/workflows/issue_wrong_template.yml b/.github/workflows/issue_wrong_template.yml index 21d10b3afcef..755975d7e330 100644 --- a/.github/workflows/issue_wrong_template.yml +++ b/.github/workflows/issue_wrong_template.yml @@ -9,8 +9,8 @@ jobs: if: github.event.label.name == 'please use the correct issue template' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - name: Setup corepack run: | npm i -g corepack@0.31 diff --git a/.github/workflows/popular.yml b/.github/workflows/popular.yml index 8224d325d0fb..a4509abf5a8b 100644 --- a/.github/workflows/popular.yml +++ b/.github/workflows/popular.yml @@ -10,8 +10,8 @@ jobs: if: github.repository_owner == 'vercel' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - name: Setup corepack run: | npm i -g corepack@0.31 diff --git a/.github/workflows/pr_ci_comment.yml b/.github/workflows/pr_ci_comment.yml index e175fc7ebf08..6f3ef7f7c69b 100644 --- a/.github/workflows/pr_ci_comment.yml +++ b/.github/workflows/pr_ci_comment.yml @@ -16,7 +16,7 @@ jobs: if: ${{ github.event.workflow_run.event == 'pull_request' && github.repository == 'vercel/next.js' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: persist-credentials: false diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml index 479c1ed2688c..0c8412ad10e0 100644 --- a/.github/workflows/pull_request_stats.yml +++ b/.github/workflows/pull_request_stats.yml @@ -96,7 +96,7 @@ jobs: bundler: [webpack, turbopack] runs-on: ubuntu-latest-16-core-oss steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 @@ -137,7 +137,7 @@ jobs: if: always() && needs.stats.result != 'cancelled' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 @@ -155,7 +155,7 @@ jobs: - name: Setup Node.js if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} diff --git a/.github/workflows/release-next-rspack.yml b/.github/workflows/release-next-rspack.yml index 580a7c327d7f..3ba6c6694f89 100644 --- a/.github/workflows/release-next-rspack.yml +++ b/.github/workflows/release-next-rspack.yml @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Display release mode run: | @@ -61,7 +61,7 @@ jobs: echo " - 📦 This will PUBLISH packages to npm" fi - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' registry-url: 'https://registry.npmjs.org' @@ -77,7 +77,7 @@ jobs: working-directory: ./rspack - name: Cache pnpm dependencies - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: path: ~/.pnpm-store key: ${{ runner.os }}-${{ runner.arch }}-pnpm-v2-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/retry_test.yml b/.github/workflows/retry_test.yml index 27f5e4068232..4e58e9ba24ad 100644 --- a/.github/workflows/retry_test.yml +++ b/.github/workflows/retry_test.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Check conclusion of required job id: required_job_conclusion - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -95,7 +95,7 @@ jobs: steps: - name: Check conclusion of required job id: required_job_conclusion - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rspack-update-tests-manifest.yml b/.github/workflows/rspack-update-tests-manifest.yml index 7faeb8db242e..86fad5af8377 100644 --- a/.github/workflows/rspack-update-tests-manifest.yml +++ b/.github/workflows/rspack-update-tests-manifest.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -32,14 +32,14 @@ jobs: GH_TOKEN: ${{ steps.release-app-token.outputs.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: # Commits made with the default `GITHUB_TOKEN` won't trigger workflows. # See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow token: ${{ steps.release-app-token.outputs.token }} - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -76,7 +76,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -93,14 +93,14 @@ jobs: GH_TOKEN: ${{ steps.release-app-token.outputs.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: # Commits made with the default `GITHUB_TOKEN` won't trigger workflows. # See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow token: ${{ steps.release-app-token.outputs.token }} - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true diff --git a/.github/workflows/sync_backport_canary_release.yml b/.github/workflows/sync_backport_canary_release.yml index 0ba5721600bc..dba40225c6cf 100644 --- a/.github/workflows/sync_backport_canary_release.yml +++ b/.github/workflows/sync_backport_canary_release.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Check whether head commit is a stable release id: precheck - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | if (context.eventName === 'workflow_dispatch') { @@ -61,7 +61,7 @@ jobs: core.setOutput('should_dispatch', 'false') core.setOutput('reason', 'Head commit is not a stable release commit') - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 if: steps.precheck.outputs.should_evaluate == 'true' with: ref: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'canary' }} @@ -69,7 +69,7 @@ jobs: - name: Setup node if: steps.precheck.outputs.should_evaluate == 'true' - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20 check-latest: true @@ -87,7 +87,7 @@ jobs: - name: Create GitHub App token id: release-app-token if: steps.precheck.outputs.should_evaluate == 'true' - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -122,7 +122,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -130,7 +130,7 @@ jobs: repositories: next.js permission-actions: write - - uses: actions/github-script@v7 + - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ steps.release-app-token.outputs.token }} script: | diff --git a/.github/workflows/test-turbopack-rust-bench-test.yml b/.github/workflows/test-turbopack-rust-bench-test.yml index 7a77e4cae78f..c4dfddba1e12 100644 --- a/.github/workflows/test-turbopack-rust-bench-test.yml +++ b/.github/workflows/test-turbopack-rust-bench-test.yml @@ -31,13 +31,13 @@ jobs: if: inputs.os == 'windows' - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup Rust uses: ./.github/actions/setup-rust - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true diff --git a/.github/workflows/test_e2e_deploy_release.yml b/.github/workflows/test_e2e_deploy_release.yml index c33c6380b2e9..c7aadf6af89c 100644 --- a/.github/workflows/test_e2e_deploy_release.yml +++ b/.github/workflows/test_e2e_deploy_release.yml @@ -52,7 +52,7 @@ jobs: next-version: ${{ steps.version.outputs.value }} steps: - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -63,7 +63,7 @@ jobs: corepack enable - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 @@ -232,7 +232,7 @@ jobs: runs-on: ubuntu-latest name: Report test results to datadog steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | .github @@ -271,10 +271,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20.9.0 check-latest: true diff --git a/.github/workflows/test_e2e_project_reset_cron.yml b/.github/workflows/test_e2e_project_reset_cron.yml index c6393a4ba97b..bdef90ce032a 100644 --- a/.github/workflows/test_e2e_project_reset_cron.yml +++ b/.github/workflows/test_e2e_project_reset_cron.yml @@ -27,7 +27,7 @@ jobs: if: github.repository_owner == 'vercel' steps: - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -38,7 +38,7 @@ jobs: corepack enable - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 diff --git a/.github/workflows/test_examples.yml b/.github/workflows/test_examples.yml index 38a6b1f49f77..da5a273266f8 100644 --- a/.github/workflows/test_examples.yml +++ b/.github/workflows/test_examples.yml @@ -27,7 +27,7 @@ jobs: matrix: node: [20, 22] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 25 # https://github.com/actions/virtual-environments/issues/1187 @@ -35,7 +35,7 @@ jobs: run: sudo ethtool -K eth0 tx off rx off - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 check-latest: true diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 6536702f59db..495e314e41c9 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -47,14 +47,14 @@ jobs: steps: - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 check-latest: true - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -111,7 +111,7 @@ jobs: - id: get-store-path run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 timeout-minutes: 5 id: cache-pnpm-store with: diff --git a/.github/workflows/turbopack-benchmark.yml b/.github/workflows/turbopack-benchmark.yml index 85828bee8c8c..b79b5c9ff3fa 100644 --- a/.github/workflows/turbopack-benchmark.yml +++ b/.github/workflows/turbopack-benchmark.yml @@ -35,7 +35,7 @@ jobs: name: Benchmark Rust Crates (small apps) runs-on: ubuntu-latest-16-core-oss steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Setup Rust toolchain uses: ./.github/actions/setup-rust @@ -70,7 +70,7 @@ jobs: name: Benchmark Rust Crates (analyzer) runs-on: ubuntu-latest-16-core-oss steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Setup Rust toolchain uses: ./.github/actions/setup-rust @@ -106,7 +106,7 @@ jobs: if: ${{ github.event.label.name == 'benchmark' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest-16-core-oss steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Setup Rust toolchain uses: ./.github/actions/setup-rust diff --git a/.github/workflows/turbopack-update-tests-manifest.yml b/.github/workflows/turbopack-update-tests-manifest.yml index 46bf1304c9ad..75fe9f9d84da 100644 --- a/.github/workflows/turbopack-update-tests-manifest.yml +++ b/.github/workflows/turbopack-update-tests-manifest.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -32,14 +32,14 @@ jobs: GH_TOKEN: ${{ steps.release-app-token.outputs.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: # Commits made with the default `GITHUB_TOKEN` won't trigger workflows. # See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow token: ${{ steps.release-app-token.outputs.token }} - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -72,7 +72,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -89,14 +89,14 @@ jobs: GH_TOKEN: ${{ steps.release-app-token.outputs.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: # Commits made with the default `GITHUB_TOKEN` won't trigger workflows. # See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow token: ${{ steps.release-app-token.outputs.token }} - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true diff --git a/.github/workflows/update_fonts_data.yml b/.github/workflows/update_fonts_data.yml index 3592891b0546..4b2ce3e332bd 100644 --- a/.github/workflows/update_fonts_data.yml +++ b/.github/workflows/update_fonts_data.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -34,14 +34,14 @@ jobs: GH_TOKEN: ${{ steps.release-app-token.outputs.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: # Commits made with the default `GITHUB_TOKEN` won't trigger workflows. # See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow token: ${{ steps.release-app-token.outputs.token }} - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true diff --git a/.github/workflows/update_react.yml b/.github/workflows/update_react.yml index f47528919e45..0b951ff07228 100644 --- a/.github/workflows/update_react.yml +++ b/.github/workflows/update_react.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Create GitHub App token id: release-app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ vars.RELEASE_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }} @@ -40,14 +40,14 @@ jobs: GH_TOKEN: ${{ steps.release-app-token.outputs.token }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: # Commits made with the default `GITHUB_TOKEN` won't trigger workflows. # See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow token: ${{ steps.release-app-token.outputs.token }} - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true diff --git a/.github/workflows/update_react_poller.yml b/.github/workflows/update_react_poller.yml index e5cdab05992f..ff1180d38611 100644 --- a/.github/workflows/update_react_poller.yml +++ b/.github/workflows/update_react_poller.yml @@ -32,7 +32,7 @@ jobs: - name: Check whether this version was already seen id: cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: # The key encodes the version. Cache hit = already processed; cache miss = new version. # The path just needs to exist so the post-step can save the cache entry on a miss. diff --git a/.github/workflows/upload-tests-manifest.yml b/.github/workflows/upload-tests-manifest.yml index b8bee13eb700..8bb155e4ad74 100644 --- a/.github/workflows/upload-tests-manifest.yml +++ b/.github/workflows/upload-tests-manifest.yml @@ -19,13 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Setup corepack run: | diff --git a/.github/workflows/upload_preview_tarballs.yml b/.github/workflows/upload_preview_tarballs.yml index b140e94d5c4c..ac7b00df1c8c 100644 --- a/.github/workflows/upload_preview_tarballs.yml +++ b/.github/workflows/upload_preview_tarballs.yml @@ -24,7 +24,7 @@ jobs: environment: preview-builds steps: - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true @@ -32,7 +32,7 @@ jobs: # Checkout from the default branch (canary) -- workflow_run always uses # the default branch's version of the workflow file and this checkout # matches that, ensuring the upload script is trusted. - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.repository.default_branch }} fetch-depth: 1 @@ -44,7 +44,7 @@ jobs: run: corepack prepare - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: path: ~/.pnpm-store key: ${{ runner.os }}-${{ runner.arch }}-pnpm-v2-${{ diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index c5c1c304e80e..b24638776db3 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -860,7 +860,6 @@ impl AppProject { None, ResolveErrorMode::Error, ) - .to_resolved() .await? .first_module() .await? diff --git a/crates/next-api/src/next_server_nft.rs b/crates/next-api/src/next_server_nft.rs index 409b5e01885b..5335de042fe5 100644 --- a/crates/next-api/src/next_server_nft.rs +++ b/crates/next-api/src/next_server_nft.rs @@ -118,9 +118,6 @@ impl Asset for ServerNftJsonAsset { .try_join() .await?; - // A few hardcoded files (not recursive) - server_output_assets.push("./package.json".into()); - let next_dir = get_next_package(this.project.project_path().owned().await?).await?; for ty in ["app-page", "pages"] { let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?; @@ -245,10 +242,11 @@ impl ServerNftJsonAsset { None, ResolveErrorMode::Error, ) + .await? .primary_modules() .await? .into_iter() - .map(|m| **m)) + .map(|m| *m)) }) .try_flat_join() .await?, diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index 3fc343abee1f..1d5425ddd9e0 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -566,6 +566,7 @@ impl PagesProject { None, ) .await? + .await? .first_module() .await? .context("expected Next.js client runtime to resolve to a module")?; diff --git a/crates/next-core/src/next_client/runtime_entry.rs b/crates/next-core/src/next_client/runtime_entry.rs index 685c4e7024ac..8e96ad664d80 100644 --- a/crates/next-core/src/next_client/runtime_entry.rs +++ b/crates/next-core/src/next_client/runtime_entry.rs @@ -40,7 +40,6 @@ impl RuntimeEntry { None, ResolveErrorMode::Error, ) - .to_resolved() .await? .primary_modules() .await?; diff --git a/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs b/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs index 9cdf7df0a60b..6432e4d30c18 100644 --- a/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs +++ b/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs @@ -1,6 +1,7 @@ use std::{io::Write, iter::once}; use anyhow::{Context, Result, bail}; +use async_trait::async_trait; use indoc::writedoc; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, ValueToString, Vc}; @@ -320,12 +321,10 @@ impl ChunkItem for EcmascriptClientReferenceProxyChunkItem { self.inner_module.ident() } - #[turbo_tasks::function] fn chunking_context(&self) -> Vc> { *self.chunking_context } - #[turbo_tasks::function] fn ty(&self) -> Vc> { Vc::upcast(Vc::::default()) } @@ -336,21 +335,19 @@ impl ChunkItem for EcmascriptClientReferenceProxyChunkItem { } } +#[async_trait] #[turbo_tasks::value_impl] impl EcmascriptChunkItem for EcmascriptClientReferenceProxyChunkItem { - #[turbo_tasks::function] - fn content(&self) -> Vc { - self.inner_chunk_item.content() - } - - #[turbo_tasks::function] - fn content_with_async_module_info( + async fn content_with_async_module_info( &self, async_module_info: Option>, estimated: bool, - ) -> Vc { + ) -> Result> { self.inner_chunk_item + .into_trait_ref() + .await? .content_with_async_module_info(async_module_info, estimated) + .await } } diff --git a/crates/next-core/src/next_font/local/mod.rs b/crates/next-core/src/next_font/local/mod.rs index c43a4696de30..232bda0144da 100644 --- a/crates/next-core/src/next_font/local/mod.rs +++ b/crates/next-core/src/next_font/local/mod.rs @@ -52,24 +52,27 @@ struct NextFontLocalFontFileOptions { #[turbo_tasks::value] pub(crate) struct NextFontLocalResolvePlugin { root: FileSystemPath, + condition: ResolvedVc, } #[turbo_tasks::value_impl] impl NextFontLocalResolvePlugin { #[turbo_tasks::function] - pub fn new(root: FileSystemPath) -> Vc { - NextFontLocalResolvePlugin { root }.cell() + pub async fn new(root: FileSystemPath) -> Result> { + let condition = BeforeResolvePluginCondition::from_request_glob(Glob::new( + rcstr!("{next,@vercel/turbopack-next/internal}/font/local/*"), + GlobOptions::default(), + )) + .to_resolved() + .await?; + Ok(NextFontLocalResolvePlugin { root, condition }.cell()) } } #[turbo_tasks::value_impl] impl BeforeResolvePlugin for NextFontLocalResolvePlugin { - #[turbo_tasks::function] fn before_resolve_condition(&self) -> Vc { - BeforeResolvePluginCondition::from_request_glob(Glob::new( - rcstr!("{next,@vercel/turbopack-next/internal}/font/local/*"), - GlobOptions::default(), - )) + *self.condition } #[turbo_tasks::function] diff --git a/crates/next-core/src/next_import_map.rs b/crates/next-core/src/next_import_map.rs index 6506137dcc82..0579532e00e7 100644 --- a/crates/next-core/src/next_import_map.rs +++ b/crates/next-core/src/next_import_map.rs @@ -1269,7 +1269,7 @@ pub async fn try_get_next_package( Request::parse(Pattern::Constant(rcstr!("next/package.json"))), node_cjs_resolve_options(root.clone()), ); - if let Some(source) = &*result.first_source().await? { + if let Some(source) = result.await?.first_source() { Ok(Vc::cell(Some(source.ident().await?.path.parent()))) } else { MissingNextFolderIssue { diff --git a/crates/next-core/src/next_server/resolve.rs b/crates/next-core/src/next_server/resolve.rs index f5ef06d92cfc..b9cfcb576bf5 100644 --- a/crates/next-core/src/next_server/resolve.rs +++ b/crates/next-core/src/next_server/resolve.rs @@ -42,41 +42,38 @@ pub enum ExternalPredicate { /// possible to resolve them at runtime. #[turbo_tasks::value] pub(crate) struct ExternalCjsModulesResolvePlugin { - root: FileSystemPath, predicate: ResolvedVc, import_externals: bool, + condition: ResolvedVc, } #[turbo_tasks::value_impl] impl ExternalCjsModulesResolvePlugin { #[turbo_tasks::function] - pub fn new( + pub async fn new( root: FileSystemPath, predicate: ResolvedVc, import_externals: bool, - ) -> Vc { - ExternalCjsModulesResolvePlugin { + ) -> Result> { + let condition = AfterResolvePluginCondition::new_with_glob( root, + Glob::new(rcstr!("**/node_modules/**"), GlobOptions::default()), + ) + .to_resolved() + .await?; + Ok(ExternalCjsModulesResolvePlugin { predicate, import_externals, + condition, } - .cell() + .cell()) } } -#[turbo_tasks::function] -fn condition(root: FileSystemPath) -> Vc { - AfterResolvePluginCondition::new_with_glob( - root, - Glob::new(rcstr!("**/node_modules/**"), GlobOptions::default()), - ) -} - #[turbo_tasks::value_impl] impl AfterResolvePlugin for ExternalCjsModulesResolvePlugin { - #[turbo_tasks::function] fn after_resolve_condition(&self) -> Vc { - condition(self.root.clone()) + *self.condition } #[turbo_tasks::function] @@ -85,7 +82,7 @@ impl AfterResolvePlugin for ExternalCjsModulesResolvePlugin { fs_path: FileSystemPath, lookup_path: FileSystemPath, reference_type: ReferenceType, - request: ResolvedVc, + request: Vc, ) -> Result> { let request_value = &*request.await?; let Request::Module { @@ -112,10 +109,7 @@ impl AfterResolvePlugin for ExternalCjsModulesResolvePlugin { let predicate = self.predicate.await?; let must_be_external = match &*predicate { ExternalPredicate::AllExcept(exceptions) => { - if *condition(self.root.clone()) - .matches(lookup_path.clone()) - .await? - { + if self.condition.await?.matches(&lookup_path) { return Ok(ResolveResultOption::none()); } @@ -214,7 +208,7 @@ impl AfterResolvePlugin for ExternalCjsModulesResolvePlugin { Ok(ResolveResultOption::none()) }; - let mut request = *request; + let mut request = request; let mut request_str = request_str.to_string(); let node_resolve_options = if is_esm { @@ -230,7 +224,7 @@ impl AfterResolvePlugin for ExternalCjsModulesResolvePlugin { node_resolve_options, ); let Some(result_from_original_location) = - *node_resolved_from_original_location.first_source().await? + node_resolved_from_original_location.await?.first_source() else { if is_esm && !package_subpath.is_empty() @@ -288,7 +282,7 @@ impl AfterResolvePlugin for ExternalCjsModulesResolvePlugin { request, node_resolve_options, ); - let resolves_equal = if let Some(result) = *node_resolved.first_source().await? { + let resolves_equal = if let Some(result) = node_resolved.await?.first_source() { let ident = result.ident().await?; let cjs_path = &ident.path; cjs_path == path diff --git a/crates/next-core/src/next_shared/resolve.rs b/crates/next-core/src/next_shared/resolve.rs index 029a2f73a2e2..70cacf712c06 100644 --- a/crates/next-core/src/next_shared/resolve.rs +++ b/crates/next-core/src/next_shared/resolve.rs @@ -92,33 +92,35 @@ impl Issue for InvalidImportModuleIssue { #[turbo_tasks::value] pub(crate) struct NextExternalResolvePlugin { - project_path: FileSystemPath, + condition: ResolvedVc, } #[turbo_tasks::value_impl] impl NextExternalResolvePlugin { #[turbo_tasks::function] - pub fn new(project_path: FileSystemPath) -> Vc { - NextExternalResolvePlugin { project_path }.cell() + pub async fn new(project_path: FileSystemPath) -> Result> { + let condition = AfterResolvePluginCondition::new_with_glob( + project_path.root().owned().await?, + Glob::new( + rcstr!("**/next/dist/**/*.{external,runtime.dev,runtime.prod}.js"), + GlobOptions::default(), + ), + ) + .to_resolved() + .await?; + Ok(NextExternalResolvePlugin { condition }.cell()) } } #[turbo_tasks::value_impl] impl AfterResolvePlugin for NextExternalResolvePlugin { - #[turbo_tasks::function] - async fn after_resolve_condition(&self) -> Result> { - Ok(AfterResolvePluginCondition::new_with_glob( - self.project_path.root().owned().await?, - Glob::new( - rcstr!("**/next/dist/**/*.{external,runtime.dev,runtime.prod}.js"), - GlobOptions::default(), - ), - )) + fn after_resolve_condition(&self) -> Vc { + *self.condition } #[turbo_tasks::function] async fn after_resolve( - &self, + self: Vc, fs_path: FileSystemPath, _lookup_path: FileSystemPath, _reference_type: ReferenceType, @@ -146,33 +148,38 @@ impl AfterResolvePlugin for NextExternalResolvePlugin { #[turbo_tasks::value] pub(crate) struct NextNodeSharedRuntimeResolvePlugin { - root: FileSystemPath, server_context_type: ServerContextType, + condition: ResolvedVc, } #[turbo_tasks::value_impl] impl NextNodeSharedRuntimeResolvePlugin { #[turbo_tasks::function] - pub fn new(root: FileSystemPath, server_context_type: ServerContextType) -> Vc { - NextNodeSharedRuntimeResolvePlugin { - root, + pub async fn new( + root: FileSystemPath, + server_context_type: ServerContextType, + ) -> Result> { + let condition = AfterResolvePluginCondition::new_with_glob( + root.root().owned().await?, + Glob::new( + rcstr!("**/next/dist/**/*.shared-runtime.js"), + GlobOptions::default(), + ), + ) + .to_resolved() + .await?; + Ok(NextNodeSharedRuntimeResolvePlugin { server_context_type, + condition, } - .cell() + .cell()) } } #[turbo_tasks::value_impl] impl AfterResolvePlugin for NextNodeSharedRuntimeResolvePlugin { - #[turbo_tasks::function] - async fn after_resolve_condition(&self) -> Result> { - Ok(AfterResolvePluginCondition::new_with_glob( - self.root.root().owned().await?, - Glob::new( - rcstr!("**/next/dist/**/*.shared-runtime.js"), - GlobOptions::default(), - ), - )) + fn after_resolve_condition(&self) -> Vc { + *self.condition } #[turbo_tasks::function] @@ -225,32 +232,34 @@ impl AfterResolvePlugin for NextNodeSharedRuntimeResolvePlugin { /// telemetry events if there is a match. #[turbo_tasks::value] pub(crate) struct ModuleFeatureReportResolvePlugin { - root: FileSystemPath, + condition: ResolvedVc, } #[turbo_tasks::value_impl] impl ModuleFeatureReportResolvePlugin { #[turbo_tasks::function] - pub fn new(root: FileSystemPath) -> Vc { - ModuleFeatureReportResolvePlugin { root }.cell() + pub async fn new(_root: FileSystemPath) -> Result> { + let condition = BeforeResolvePluginCondition::from_modules(Vc::cell( + FEATURE_MODULES + .keys() + .map(|k| (*k).into()) + .collect::>(), + )) + .to_resolved() + .await?; + Ok(ModuleFeatureReportResolvePlugin { condition }.cell()) } } #[turbo_tasks::value_impl] impl BeforeResolvePlugin for ModuleFeatureReportResolvePlugin { - #[turbo_tasks::function] fn before_resolve_condition(&self) -> Vc { - BeforeResolvePluginCondition::from_modules(Vc::cell( - FEATURE_MODULES - .keys() - .map(|k| (*k).into()) - .collect::>(), - )) + *self.condition } #[turbo_tasks::function] async fn before_resolve( - &self, + self: Vc, _lookup_path: FileSystemPath, _reference_type: ReferenceType, request: Vc, @@ -269,6 +278,7 @@ impl BeforeResolvePlugin for ModuleFeatureReportResolvePlugin { .find(|sub_path| path.is_match(sub_path)); if let Some(sub_path) = sub_path { + // This is not accurate. we only emit one diagnostic per request not per import ModuleFeatureTelemetry::new(format!("{module}{sub_path}").into(), 1) .resolved_cell() .emit(); @@ -282,33 +292,35 @@ impl BeforeResolvePlugin for ModuleFeatureReportResolvePlugin { #[turbo_tasks::value] pub(crate) struct NextSharedRuntimeResolvePlugin { - root: FileSystemPath, + condition: ResolvedVc, } #[turbo_tasks::value_impl] impl NextSharedRuntimeResolvePlugin { #[turbo_tasks::function] - pub fn new(root: FileSystemPath) -> Vc { - NextSharedRuntimeResolvePlugin { root }.cell() + pub async fn new(root: FileSystemPath) -> Result> { + let condition = AfterResolvePluginCondition::new_with_glob( + root.root().owned().await?, + Glob::new( + rcstr!("**/next/dist/esm/**/*.shared-runtime.js"), + GlobOptions::default(), + ), + ) + .to_resolved() + .await?; + Ok(NextSharedRuntimeResolvePlugin { condition }.cell()) } } #[turbo_tasks::value_impl] impl AfterResolvePlugin for NextSharedRuntimeResolvePlugin { - #[turbo_tasks::function] - async fn after_resolve_condition(&self) -> Result> { - Ok(AfterResolvePluginCondition::new_with_glob( - self.root.root().owned().await?, - Glob::new( - rcstr!("**/next/dist/esm/**/*.shared-runtime.js"), - GlobOptions::default(), - ), - )) + fn after_resolve_condition(&self) -> Vc { + *self.condition } #[turbo_tasks::function] async fn after_resolve( - &self, + self: Vc, fs_path: FileSystemPath, _lookup_path: FileSystemPath, _reference_type: ReferenceType, diff --git a/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs b/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs index a57c35f632bf..250fdcb6a7dd 100644 --- a/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs +++ b/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs @@ -135,8 +135,10 @@ pub async fn get_swc_ecma_transform_rule_impl( ) .await?; - let Some(plugin_module) = - &*plugin_wasm_module_resolve_result.first_module().await? + let Some(plugin_module) = plugin_wasm_module_resolve_result + .await? + .first_module() + .await? else { // Ignore unresolvable plugin modules, handle_resolve_error has already emitted // an issue. diff --git a/crates/next-core/src/next_shared/webpack_rules/babel.rs b/crates/next-core/src/next_shared/webpack_rules/babel.rs index ae6a99226bbe..d208cd00a278 100644 --- a/crates/next-core/src/next_shared/webpack_rules/babel.rs +++ b/crates/next-core/src/next_shared/webpack_rules/babel.rs @@ -260,7 +260,7 @@ async fn detect_react_compiler_target( node_cjs_resolve_options(project_path.root().owned().await?), ); - let Some(source) = &*react_pkg_result.first_source().await? else { + let Some(source) = react_pkg_result.await?.first_source() else { return Ok(None); }; @@ -335,7 +335,7 @@ pub async fn resolve_babel_plugin_react_compiler( Request::parse(Pattern::Constant(BABEL_PLUGIN_REACT_COMPILER_PACKAGE_JSON)), node_cjs_resolve_options(project_path.root().owned().await?), ); - let Some(source) = &*babel_plugin_result.first_source().await? else { + let Some(source) = babel_plugin_result.await?.first_source() else { BabelPluginReactCompilerResolutionIssue { failed_resolution: BABEL_PLUGIN_REACT_COMPILER, config_file_path: next_config diff --git a/lerna.json b/lerna.json index 22dafe4e2d77..621ee8024a0c 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.3.0-canary.16" + "version": "16.3.0-canary.17" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 136b7bd70668..a8f8ffcde9e1 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 0f316ab182ce..4be8cc2a367b 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.3.0-canary.16", + "@next/eslint-plugin-next": "16.3.0-canary.17", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 8d21dfefc980..10b654cc88ac 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index a9488918943b..cf85666421a8 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index e96f16be3ef4..a141fd3fe19f 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 6b4ea5626d97..c89b6d65819a 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index c94d9dcda3f2..13bf9b27a458 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index ab03bd5ec2b3..eb85f323cfe1 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 533fe43c34dc..da44dcd0fbe3 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index 5383d20fdeb8..51fad7280809 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "repository": { "url": "vercel/next.js", "directory": "packages/next-playwright" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 706d149aa7ce..4e919113274a 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 7a8896237837..3577de9c8850 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 6e778a00dfa3..0acb272a5685 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 4e2112ffe7bc..a968dd9de926 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 681582e1689b..47e9c596ec4f 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index e6eb2f8dfdad..b15ad624f3bb 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 39c8efc6b79c..444ff4784dba 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -101,7 +101,7 @@ ] }, "dependencies": { - "@next/env": "16.3.0-canary.16", + "@next/env": "16.3.0-canary.17", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -165,11 +165,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.3.0-canary.16", - "@next/polyfill-module": "16.3.0-canary.16", - "@next/polyfill-nomodule": "16.3.0-canary.16", - "@next/react-refresh-utils": "16.3.0-canary.16", - "@next/swc": "16.3.0-canary.16", + "@next/font": "16.3.0-canary.17", + "@next/polyfill-module": "16.3.0-canary.17", + "@next/polyfill-nomodule": "16.3.0-canary.17", + "@next/react-refresh-utils": "16.3.0-canary.17", + "@next/swc": "16.3.0-canary.17", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.58.2", "@rspack/core": "1.6.7", diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index ef751bf7ec4a..0c436dcf9908 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1852,6 +1852,9 @@ export default async function build( appDir: dir, relativeAppDir: path.relative(outputFileTracingRoot, dir), files: [ + // distDir `{"type":"commonjs"}` boundary so `.next/server/**/*.js` + // is loaded as CJS when the user's project is "type": "module". + 'package.json', ROUTES_MANIFEST, path.relative(distDir, pagesManifestPath), BUILD_MANIFEST, diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index 0f0341b8b12a..6fc58f71b53f 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -48,7 +48,6 @@ import { RSC_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, - NEXT_INSTANT_PREFETCH_HEADER, NEXT_INSTANT_TEST_COOKIE, NEXT_IS_PRERENDER_HEADER, NEXT_DID_POSTPONE_HEADER, @@ -420,17 +419,20 @@ export async function handler( // Enable the Instant Navigation Testing API. Renders only the prefetched // portion of the page, excluding dynamic content. This allows tests to // assert on the prefetched UI state deterministically. - // - Header: Used for client-side navigations where we can set request headers - // - Cookie: Used for MPA navigations (page reload, full page load) where we - // can't set request headers. Only applies to document requests (no RSC - // header) - RSC requests should proceed normally even during a locked scope, - // with blocking happening on the client side. + // + // The instant test cookie is sent automatically with all requests while a + // navigation lock is held. We treat a request as a test render when the + // cookie is present and either: + // - it's a document request (no RSC header) — covers MPA navigations + // - it's a prefetch RSC request — covers client-side prefetches + // Regular RSC navigation requests proceed normally even during a locked + // scope; blocking happens on the client side. const isInstantNavigationTest = exposeTestingApi && - (req.headers[NEXT_INSTANT_PREFETCH_HEADER] === '1' || - (!isRSCRequestHeader(req.headers[RSC_HEADER]) && - typeof req.headers.cookie === 'string' && - req.headers.cookie.includes(NEXT_INSTANT_TEST_COOKIE + '='))) + typeof req.headers.cookie === 'string' && + req.headers.cookie.includes(NEXT_INSTANT_TEST_COOKIE + '=') && + (!isRSCRequestHeader(req.headers[RSC_HEADER]) || + req.headers[NEXT_ROUTER_PREFETCH_HEADER] === '1') // This page supports PPR if it is marked as being `PARTIALLY_STATIC` in the // prerender manifest and this is an app page. diff --git a/packages/next/src/client/components/app-router-headers.ts b/packages/next/src/client/components/app-router-headers.ts index f39aa6dbd460..ae5b1b788271 100644 --- a/packages/next/src/client/components/app-router-headers.ts +++ b/packages/next/src/client/components/app-router-headers.ts @@ -16,17 +16,9 @@ export const NEXT_HMR_REFRESH_HASH_COOKIE = '__next_hmr_refresh_hash__' as const export const NEXT_URL = 'next-url' as const export const RSC_CONTENT_TYPE_HEADER = 'text/x-component' as const -// Header for the Instant Navigation Testing API. In development and testing -// builds, static pre-renders normally don't happen. This header tells the -// server to perform a static pre-render anyway, allowing tests to assert on -// the prefetched UI. Not exposed in production builds by default. -export const NEXT_INSTANT_PREFETCH_HEADER = - 'next-instant-navigation-testing-prefetch' as const - -// Cookie for the Instant Navigation Testing API. Used for MPA navigations -// (page reload, full page load) where we can't set request headers. When set, -// the server renders only the static shell. Not exposed in production builds -// by default. +// Cookie for the Instant Navigation Testing API. Sent automatically with all +// requests while a navigation lock is held; the server uses its presence to +// render only the static shell. Not exposed in production builds by default. export const NEXT_INSTANT_TEST_COOKIE = 'next-instant-navigation-testing' as const diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 576c6491c276..8f4861321f96 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -18,7 +18,6 @@ import type { import { type NEXT_ROUTER_PREFETCH_HEADER, type NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, - type NEXT_INSTANT_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_RSC_UNION_QUERY, NEXT_URL, @@ -110,7 +109,6 @@ export type RequestHeaders = { 'Next-Test-Fetch-Priority'?: RequestInit['priority'] [NEXT_HTML_REQUEST_ID_HEADER]?: string // dev-only [NEXT_REQUEST_ID_HEADER]?: string // dev-only - [NEXT_INSTANT_PREFETCH_HEADER]?: '1' // testing API only } function doMpaNavigation(url: string): FetchServerResponseResult { diff --git a/packages/next/src/client/components/segment-cache/cache.ts b/packages/next/src/client/components/segment-cache/cache.ts index 35072e47a5b3..d0ecf46e1cdb 100644 --- a/packages/next/src/client/components/segment-cache/cache.ts +++ b/packages/next/src/client/components/segment-cache/cache.ts @@ -16,7 +16,6 @@ import { } from '../../../shared/lib/segment-cache/vary-params-decoding' import { NEXT_DID_POSTPONE_HEADER, - NEXT_INSTANT_PREFETCH_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_ROUTER_STALE_TIME_HEADER, @@ -1598,9 +1597,6 @@ export async function fetchRouteOnCacheMiss( if (nextUrl !== null) { headers[NEXT_URL] = nextUrl } - // Tell the server to perform a static pre-render for the Instant Navigation - // Testing API. Static pre-renders don't normally happen during development. - addInstantPrefetchHeaderIfLocked(headers) try { const url = new URL(pathname + search, location.origin) @@ -1930,9 +1926,6 @@ export async function fetchSegmentsOnCacheMiss( if (nextUrl !== null) { headers[NEXT_URL] = nextUrl } - // Tell the server to perform a static pre-render for the Instant Navigation - // Testing API. Static pre-renders don't normally happen during development. - addInstantPrefetchHeaderIfLocked(headers) const requestUrl = isOutputExportMode ? // In output: "export" mode, we need to add the segment path to the URL. @@ -2871,22 +2864,6 @@ export function canNewFetchStrategyProvideMoreContent( return currentStrategy < newStrategy } -/** - * Adds the instant prefetch header if the navigation lock is active. - * Uses a lazy require to ensure dead code elimination. - */ -function addInstantPrefetchHeaderIfLocked( - headers: Record -): void { - if (process.env.__NEXT_EXPOSE_TESTING_API) { - const { isNavigationLocked } = - require('./navigation-testing-lock') as typeof import('./navigation-testing-lock') - if (isNavigationLocked()) { - headers[NEXT_INSTANT_PREFETCH_HEADER] = '1' - } - } -} - function getStaleAtFromHeader( now: number, response: RSCResponse diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index d8ff3f0b337c..20171ba9d3f7 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -168,6 +168,7 @@ import { DynamicHoleKind, trackThrownErrorInNavigation, createInstantValidationState, + type NavigationValidationResult, } from './dynamic-rendering' import { logBuildDebugHint } from './blocking-route-messages' import { @@ -6107,21 +6108,10 @@ async function validateInstantConfigs( const { implicitTags, nonce, workStore } = ctx const isDebugChannelEnabled = !!ctx.renderOpts.setReactDebugChannel - /** - * Build and validate a combined payload at the given URL depth. - * - * Returns null if no instant config exists at this depth. - * Returns an empty array if validation passed. - * Returns a non-empty array of errors if validation failed. - * - * When the initial validation uses static segments and finds errors, - * automatically retries with runtime stages to discriminate between - * runtime and dynamic errors, returning the more specific result. - */ async function validateAtDepth( depth: number, groupDepthForValidation: number - ): Promise | null> { + ): Promise { return validateAtDepthImpl(depth, groupDepthForValidation, null) } @@ -6129,7 +6119,7 @@ async function validateInstantConfigs( depth: number, groupDepthForValidation: number, previousBoundaryState: null | ValidationBoundaryTracking - ): Promise> { + ): Promise { const extraChunksController = new AbortController() const boundaryState = createValidationBoundaryTracking() @@ -6211,7 +6201,7 @@ async function validateInstantConfigs( validationSampleTracking, } - let errors: Array + let result: NavigationValidationResult try { const { prelude: unprocessedPrelude } = await runInSequentialTasks( () => { @@ -6307,7 +6297,7 @@ async function validateInstantConfigs( const { preludeIsEmpty } = await processPreludeOp(unprocessedPrelude) - errors = getNavigationDisallowedDynamicReasons( + result = getNavigationDisallowedDynamicReasons( workStore, preludeIsEmpty ? PreludeState.Empty : PreludeState.Full, instantValidationState, @@ -6315,7 +6305,7 @@ async function validateInstantConfigs( boundaryState ) } catch (thrownValue) { - errors = getNavigationDisallowedDynamicReasons( + result = getNavigationDisallowedDynamicReasons( workStore, PreludeState.Errored, instantValidationState, @@ -6324,28 +6314,32 @@ async function validateInstantConfigs( ) } - // This prerender did not produce any errors - if (errors.length === 0) { - return [] + // If the prerender produced no real errors at this depth — either an + // empty array (clean) or a deferred-only result (Error/AggregateError + // representing a missing-boundary fallback) — there's nothing to + // discriminate. Pass it up so the outer loop can hold any deferred + // fallback back until every depth has been tried. + if (!Array.isArray(result) || result.length === 0) { + return result } if (previousBoundaryState === null && payloadResult.hasAmbiguousErrors) { // This is the first validation attempt. we prepared a payload where dynamic holes might be runtime data dependencies // or dynamic data dependencies. We do a followup validation using a payload with only Runtime segments to discriminate - const dynamicOnlyErrors = await validateAtDepthImpl( + const dynamicOnlyResult = await validateAtDepthImpl( depth, groupDepthForValidation, boundaryState ) - if (dynamicOnlyErrors !== null && dynamicOnlyErrors.length > 0) { + if (Array.isArray(dynamicOnlyResult) && dynamicOnlyResult.length > 0) { // The dynamic errors only validation found errors to report so we favor those - return dynamicOnlyErrors + return dynamicOnlyResult } } - // If we didn't return some other errors at this point the only thing to return is this validation's errors - return errors + // If we didn't return some other errors at this point the only thing to return is this validation's result + return result } // Discover validation depth bounds from the LoaderTree. The array @@ -6354,6 +6348,8 @@ async function validateInstantConfigs( const groupDepthsByUrlDepth = discoverValidationDepths(loaderTree) const maxDepth = groupDepthsByUrlDepth.length + let impairedValidation: null | Error | AggregateError = null + for (let depth = maxDepth - 1; depth >= 0; depth--) { const maxGroupDepth = groupDepthsByUrlDepth[depth] @@ -6369,21 +6365,43 @@ async function validateInstantConfigs( : '...') ) - const errors = await validateAtDepth(depth, currentGroupDepth) + const result = await validateAtDepth(depth, currentGroupDepth) - if (errors === null) { + if (Array.isArray(result)) { + const errors: Array = result + // Validation completed at least partially. + if (errors.length > 0) { + // There were issues with producing an instant UI for this attempted navigation + debug?.( + ` Depth ${depth}+${currentGroupDepth}: ❌ Failed (${errors.length} errors)` + ) + return errors + } else { + // There is nothing blocking instant UI for this simluated navigation + debug?.(` Depth ${depth}+${currentGroupDepth}: ✅ Passed`) + } + } else if (result === null) { + // There was no validation to perform at this level debug?.(` No config at depth ${depth}+${currentGroupDepth}, skipping.`) - continue - } - - if (errors.length > 0) { - debug?.( - ` Depth ${depth}+${currentGroupDepth}: ❌ Failed (${errors.length} errors)` - ) - return errors + } else { + // Something prevented this level from fully validating but there were no detected errors + if (impairedValidation === null) { + impairedValidation = result + } } + } + } - debug?.(` Depth ${depth}+${currentGroupDepth}: ✅ Passed`) + if (impairedValidation) { + debug?.( + `⏸ All depths passed without real errors; surfacing deferred missing-boundary fallback` + ) + if (impairedValidation instanceof AggregateError) { + // There is at least one potential cause of the validation blocking + return impairedValidation.errors + } else { + // There was no known cause but we report something anyway + return [impairedValidation] } } diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 10a30c365788..efafe795a8ea 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -1318,13 +1318,27 @@ export function getStaticShellDisallowedDynamicReasons( return [] } +/** + * `errors` are validation failures that should be surfaced immediately. + * `deferredFallback` carries a missing-boundary explanation that the caller + * should hold back until *every* validation depth has been tried — a missing + * boundary often just means a parent layout intentionally omitted a slot, and + * a different depth's validation may surface a more meaningful error. + */ +export type NavigationValidationResult = + // instances that block instant navigation + | Array + // validation was blocked with zero or more reasons + | Error + | AggregateError + export function getNavigationDisallowedDynamicReasons( workStore: WorkStore, prelude: PreludeState, dynamicValidation: InstantValidationState, validationSampleTracking: InstantValidationSampleTracking | null, boundaryState: ValidationBoundaryTracking -): Array { +): NavigationValidationResult { // If we have errors related to missing samples, those should take precedence over everything else. if (validationSampleTracking) { const { missingSampleErrors } = validationSampleTracking @@ -1338,30 +1352,6 @@ export function getNavigationDisallowedDynamicReasons( return validationPreventingErrors } - if (!allRequiredBoundariesRendered(boundaryState)) { - const { thrownErrorsOutsideBoundary } = dynamicValidation - const rootInstantStack = dynamicValidation.slotStacks[0] - if (thrownErrorsOutsideBoundary.length === 0) { - const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.` - const error = rootInstantStack !== null ? rootInstantStack() : new Error() - error.name = 'Error' - error.message = message - return [error] - } else if (thrownErrorsOutsideBoundary.length === 1) { - const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.` - const error = rootInstantStack !== null ? rootInstantStack() : new Error() - error.name = 'Error' - error.message = message - return [error, thrownErrorsOutsideBoundary[0] as Error] - } else { - const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.` - const error = rootInstantStack !== null ? rootInstantStack() : new Error() - error.name = 'Error' - error.message = message - return [error, ...(thrownErrorsOutsideBoundary as Error[])] - } - } - // NOTE: We don't care about Suspense above body here, // we're only concerned with the validation boundary if (prelude !== PreludeState.Full) { @@ -1370,18 +1360,17 @@ export function getNavigationDisallowedDynamicReasons( return dynamicErrors } - if (prelude === PreludeState.Empty) { - // If a client component suspended prevented us from rendering a shell - // but didn't block validation, we don't require a prelude. - if (dynamicValidation.hasAllowedClientDynamicAboveBoundary) { - return [] - } - // If we ever get this far then we messed up the tracking of invalid dynamic. - return [ - new InvariantError( - `Route "${workStore.route}" failed to render during instant validation and Next.js was unable to determine a reason.` - ), - ] + if ( + prelude === PreludeState.Empty && + !dynamicValidation.hasAllowedClientDynamicAboveBoundary && + allRequiredBoundariesRendered(boundaryState) + ) { + // If we ever get this far then we messed up the tracking of invalid + // dynamic. (When boundaries are missing the deferred fallback below + // will surface a more useful error.) + return new InvariantError( + `Route "${workStore.route}" failed to render during instant validation and Next.js was unable to determine a reason.` + ) } } else { const dynamicErrors = dynamicValidation.dynamicErrors @@ -1396,6 +1385,43 @@ export function getNavigationDisallowedDynamicReasons( return [dynamicValidation.dynamicMetadata] } } + + // Missing boundaries on their own aren't a strong signal — a parent + // layout may legitimately omit a slot. Defer this so the caller can + // try shallower validation depths first; if every depth comes up + // empty we still want to surface this so the user is made aware that + // validation didn't complete. When we add a markers API, the + // marker-based variant of this check can become strict again. + if (!allRequiredBoundariesRendered(boundaryState)) { + const { thrownErrorsOutsideBoundary } = dynamicValidation + const rootInstantStack = dynamicValidation.slotStacks[0] + if (thrownErrorsOutsideBoundary.length === 0) { + const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.` + const error = rootInstantStack !== null ? rootInstantStack() : new Error() + error.name = 'Error' + error.message = message + return error + } else if (thrownErrorsOutsideBoundary.length === 1) { + const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.` + const error = rootInstantStack !== null ? rootInstantStack() : new Error() + error.name = 'Error' + error.message = message + return new AggregateError([ + error, + thrownErrorsOutsideBoundary[0] as Error, + ]) + } else { + const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.` + const error = rootInstantStack !== null ? rootInstantStack() : new Error() + error.name = 'Error' + error.message = message + return new AggregateError([ + error, + ...(thrownErrorsOutsideBoundary as Error[]), + ]) + } + } + // We had a non-empty prelude and there are no dynamic holes return [] } diff --git a/packages/next/src/server/stream-utils/node-web-streams-helper.ts b/packages/next/src/server/stream-utils/node-web-streams-helper.ts index 0922d54abf26..b088856daa63 100644 --- a/packages/next/src/server/stream-utils/node-web-streams-helper.ts +++ b/packages/next/src/server/stream-utils/node-web-streams-helper.ts @@ -19,7 +19,6 @@ import { NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_RSC_UNION_QUERY, - NEXT_INSTANT_PREFETCH_HEADER, } from '../../client/components/app-router-headers' import { computeCacheBustingSearchParam } from '../../shared/lib/router/utils/cache-busting-search-param' import type { AnyStream } from '../app-render/stream-ops' @@ -590,8 +589,9 @@ export async function createInstantTestScriptInsertionTransformStream( ): Promise> { // Kick off a fetch for the static RSC payload. This is the hydration // source for the locked static shell — same as the __NEXT_CLIENT_RESUME - // fetch used for fallback routes, but with NEXT_INSTANT_PREFETCH_HEADER - // so the server returns static-only data. + // fetch used for fallback routes. The instant test cookie is sent + // automatically with the same-origin fetch, so the server returns + // static-only data. // // The fetch promise is stored as self.__next_instant_test, which doubles // as the feature flag (truthy = instant test mode). The client processes @@ -610,7 +610,7 @@ export async function createInstantTestScriptInsertionTransformStream( // bootstrapScriptContent. const requestIdScript = requestId !== null ? `self.__next_r=${JSON.stringify(requestId)};` : '' - const INSTANT_TEST_SCRIPT = `` + const INSTANT_TEST_SCRIPT = `` let didAlreadyInsert = false return new TransformStream({ diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 23a9c4fddd91..83652a325da8 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index d7de1d72d73a..1abd9902c7c8 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.3.0-canary.16", + "version": "16.3.0-canary.17", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -27,7 +27,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.3.0-canary.16", + "next": "16.3.0-canary.17", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7470a45798ae..2c0b777dc435 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -983,7 +983,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1060,7 +1060,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1181,19 +1181,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../font '@next/polyfill-module': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../react-refresh-utils '@next/swc': - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1927,7 +1927,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.3.0-canary.16 + specifier: 16.3.0-canary.17 version: link:../next outdent: specifier: 0.8.0 diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx index aab56fb135a3..7244911112e9 100644 --- a/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx @@ -254,6 +254,9 @@ export default async function Page() {
  • +
  • + +
  • Disable Validation

    diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/layout.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/layout.tsx new file mode 100644 index 000000000000..df7ebb7b2ebb --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/layout.tsx @@ -0,0 +1,7 @@ +// Inner layout intentionally drops `{children}` so the inner page +// boundary (configured for instant validation) cannot render. This +// produces a missing-boundary fallback at the inner depth's iteration +// of the validation loop. +export default function Layout() { + return

    Children intentionally hidden to test multi-depth deferral.

    +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/page.tsx new file mode 100644 index 000000000000..cedd439f6979 --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/page.tsx @@ -0,0 +1,11 @@ +// Inner page configured for instant validation. The parent layout +// hides `{children}`, so this page never renders — its boundary will +// never appear in `boundaryState.renderedIds`, producing a missing- +// boundary fallback at the inner depth's iteration of the validation +// loop. The outer layout above is also configured but validates +// cleanly, so the deferred fallback surfaces only at the end. +export const unstable_instant = { level: 'experimental-error' } + +export default function Page() { + return

    Inner page (should never render in this fixture).

    +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/layout.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/layout.tsx new file mode 100644 index 000000000000..e0e63f77d11a --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/layout.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from 'react' + +// Outer layout: configured for instant validation, no errors. Renders +// children cleanly so this depth's iteration of the validation loop +// validates without producing errors or fallback. Used together with +// the deeper inner page config to exercise the multi-depth deferral +// path: the inner depth defers a missing-boundary fallback, this +// (shallower) depth has nothing to report, and the deferred fallback +// surfaces only after the loop has exhausted every depth. +export const unstable_instant = { level: 'experimental-error' } + +export default function Layout({ children }: { children: ReactNode }) { + return
    {children}
    +} diff --git a/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/page.tsx b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/page.tsx new file mode 100644 index 000000000000..dc41024e012f --- /dev/null +++ b/test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/page.tsx @@ -0,0 +1,5 @@ +// Landing page for the multi-depth-deferred-fallback fixture. Not +// targeted by the deferral test — only present so the route exists. +export default function Page() { + return

    multi-depth-deferred-fallback root

    +} diff --git a/test/e2e/app-dir/instant-validation/instant-validation-parallel-slots.test.ts b/test/e2e/app-dir/instant-validation/instant-validation-parallel-slots.test.ts index d66a3a3bd6a7..940e48a490f7 100644 --- a/test/e2e/app-dir/instant-validation/instant-validation-parallel-slots.test.ts +++ b/test/e2e/app-dir/instant-validation/instant-validation-parallel-slots.test.ts @@ -617,14 +617,27 @@ describe('instant validation - parallel slot configs', () => { const browser = await navigateTo(href) await expect(browser).toDisplayCollapsedRedbox(` { - "description": "Route "/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.", - "environmentLabel": "Server", - "label": "Console Error", - "source": "app/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked/page.tsx (1:33) @ unstable_instant + "cause": [ + { + "label": "Caused by: Instant Validation", + "source": "app/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked/page.tsx (1:33) @ unstable_instant > 1 | export const unstable_instant = { level: 'experimental-error' } | ^", + "stack": [ + "unstable_instant app/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked/page.tsx (1:33)", + "Set.forEach ", + ], + }, + ], + "code": "E1166", + "description": "Next.js encountered runtime data during the initial render.", + "environmentLabel": "Server", + "label": "Instant", + "source": "app/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/@breadcrumbs/blocked/page.tsx (3:16) @ BreadcrumbsPage + > 3 | await cookies() + | ^", "stack": [ - "unstable_instant app/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked/page.tsx (1:33)", + "BreadcrumbsPage app/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/@breadcrumbs/blocked/page.tsx (3:16)", ], } `) @@ -632,8 +645,20 @@ describe('instant validation - parallel slot configs', () => { const result = await prerender(href) expect(extractBuildValidationError(result.cliOutput)) .toMatchInlineSnapshot(` - "Error: Route "/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason. - at ignore-listed frames + "Error: Route "/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked": Next.js encountered runtime data during the initial render. + + \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` accessed outside of \`\` blocks navigation, leading to a slower user experience. + + Ways to fix this: + - Move the data access into a child component within a boundary + - Use \`generateStaticParams\` to make route params static + - Set \`export const instant = false\` to allow a blocking route + + Learn more: https://nextjs.org/docs/messages/blocking-route + at main () + at body () + at html () + at a () Build-time instant validation failed for route "/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked". To get a more detailed stack trace and pinpoint the issue, try one of the following: - Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked" in your browser to investigate the error. diff --git a/test/e2e/app-dir/instant-validation/instant-validation.test.ts b/test/e2e/app-dir/instant-validation/instant-validation.test.ts index 1d7eaa5348cb..2b8af700ee14 100644 --- a/test/e2e/app-dir/instant-validation/instant-validation.test.ts +++ b/test/e2e/app-dir/instant-validation/instant-validation.test.ts @@ -3302,6 +3302,57 @@ describe('instant validation', () => { }) }) + describe('multi-depth fallback deferral', () => { + // The validation outer loop iterates from deepest configured depth + // to shallowest. When the deepest iteration only produces a missing- + // boundary fallback (i.e., the configured boundary didn't render and + // there were no thrown errors), that fallback should be deferred so + // a real error from a shallower depth can win. If no shallower depth + // surfaces a real error, the deferred fallback eventually surfaces + // so the user is still made aware that validation didn't complete. + + it('surfaces deferred fallback when no shallower depth has a real error', async () => { + // Outer layout has unstable_instant and validates cleanly. Inner + // page has unstable_instant but its parent layout drops {children}, + // so the inner boundary can't render. Without the deferral, we'd + // bail out after the deepest iteration; with deferral, the outer + // iteration runs cleanly and the deferred fallback then surfaces. + if (isNextDev) { + const browser = await navigateTo( + '/suspense-in-root/static/multi-depth-deferred-fallback/inner' + ) + await expect(browser).toDisplayCollapsedRedbox(` + { + "description": "Route "/suspense-in-root/static/multi-depth-deferred-fallback/inner": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/suspense-in-root/static/multi-depth-deferred-fallback/inner/page.tsx (7:33) @ unstable_instant + > 7 | export const unstable_instant = { level: 'experimental-error' } + | ^", + "stack": [ + "unstable_instant app/suspense-in-root/static/multi-depth-deferred-fallback/inner/page.tsx (7:33)", + ], + } + `) + } else { + const result = await prerender( + '/suspense-in-root/static/multi-depth-deferred-fallback/inner' + ) + expect(extractBuildValidationError(result.cliOutput)) + .toMatchInlineSnapshot(` + "Error: Route "/suspense-in-root/static/multi-depth-deferred-fallback/inner": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason. + at ignore-listed frames + Build-time instant validation failed for route "/suspense-in-root/static/multi-depth-deferred-fallback/inner". + To get a more detailed stack trace and pinpoint the issue, try one of the following: + - Start the app in development mode by running \`next dev\`, then open "/suspense-in-root/static/multi-depth-deferred-fallback/inner" in your browser to investigate the error. + - Rerun the production build with \`next build --debug-prerender\` to generate better stack traces. + Stopping prerender due to instant validation errors." + `) + expect(result.exitCode).toBe(1) + } + }) + }) + describe('disabling validation', () => { it('in a layout', async () => { if (isNextDev) { diff --git a/test/production/required-server-files-package-boundary/app/layout.tsx b/test/production/required-server-files-package-boundary/app/layout.tsx new file mode 100644 index 000000000000..08eaa94fdc88 --- /dev/null +++ b/test/production/required-server-files-package-boundary/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/production/required-server-files-package-boundary/app/page.tsx b/test/production/required-server-files-package-boundary/app/page.tsx new file mode 100644 index 000000000000..ff7159d9149f --- /dev/null +++ b/test/production/required-server-files-package-boundary/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

    hello world

    +} diff --git a/test/production/required-server-files-package-boundary/my-adapter.mjs b/test/production/required-server-files-package-boundary/my-adapter.mjs new file mode 100644 index 000000000000..05c9cff12996 --- /dev/null +++ b/test/production/required-server-files-package-boundary/my-adapter.mjs @@ -0,0 +1,25 @@ +import fs from 'fs' +import path from 'path' + +/** @type {import('next').NextAdapter } */ +const myAdapter = { + name: 'package-boundary-test-adapter', + onBuildComplete: async (ctx) => { + // Capture every page output's assets so the test can assert the distDir + // package.json boundary marker is reachable from the page bundle's + // function root. Without this, Node walks up to the user's + // "type": "module" package.json and loads `.next/server/**/*.js` as ESM. + const pageAssets = {} + + for (const output of [...ctx.outputs.pages, ...ctx.outputs.appPages]) { + pageAssets[output.id] = Object.keys(output.assets || {}).sort() + } + + await fs.promises.writeFile( + path.join(ctx.distDir, 'adapter-page-assets.json'), + JSON.stringify(pageAssets, null, 2) + ) + }, +} + +export default myAdapter diff --git a/test/production/required-server-files-package-boundary/next.config.mjs b/test/production/required-server-files-package-boundary/next.config.mjs new file mode 100644 index 000000000000..4f4274978bc2 --- /dev/null +++ b/test/production/required-server-files-package-boundary/next.config.mjs @@ -0,0 +1,9 @@ +import Module from 'module' +const require = Module.createRequire(import.meta.url) + +/** @type {import('next').NextConfig} */ +const nextConfig = { + adapterPath: require.resolve('./my-adapter.mjs'), +} + +export default nextConfig diff --git a/test/production/required-server-files-package-boundary/required-server-files-package-boundary.test.ts b/test/production/required-server-files-package-boundary/required-server-files-package-boundary.test.ts new file mode 100644 index 000000000000..9f893dd69291 --- /dev/null +++ b/test/production/required-server-files-package-boundary/required-server-files-package-boundary.test.ts @@ -0,0 +1,48 @@ +import { nextTestSetup } from 'e2e-utils' +import { join } from 'path' + +describe('distDir package.json commonjs boundary', () => { + const { next } = nextTestSetup({ + files: __dirname, + // Use "type": "module" in the project package.json so the boundary file + // is what determines whether `.next/server/**/*.js` is loaded as CJS. + packageJson: { + type: 'module', + }, + }) + + it('writes the distDir package.json with the commonjs boundary', async () => { + const distPackageJson = JSON.parse( + await next.readFile('.next/package.json') + ) + expect(distPackageJson).toEqual({ type: 'commonjs' }) + }) + + it('lists the distDir package.json in required-server-files.json', async () => { + const manifest = JSON.parse( + await next.readFile('.next/required-server-files.json') + ) + // Adapters consume `requiredServerFiles.files` to seed the per-page + // shared assets. The boundary marker must be in this list because + // per-page nft traces do not include it. + expect(manifest.files).toContain(join('.next', 'package.json')) + }) + + it('includes the distDir package.json in every adapter page output assets', async () => { + // The adapter writes per-page asset keys to `.next/adapter-page-assets.json` + // during onBuildComplete. The fix in `build/index.ts` ensures the boundary + // is in `requiredServerFiles.files`, which `handleBuildComplete` then + // merges into every page output's `assets` map. + const pageAssets = JSON.parse( + await next.readFile('.next/adapter-page-assets.json') + ) + + const pageIds = Object.keys(pageAssets) + expect(pageIds.length).toBeGreaterThan(0) + + const boundaryPath = join('.next', 'package.json') + for (const pageId of pageIds) { + expect(pageAssets[pageId]).toContain(boundaryPath) + } + }) +}) diff --git a/test/production/standalone-mode/type-module/index.test.ts b/test/production/standalone-mode/type-module/index.test.ts index 153954038261..a0c319ad6065 100644 --- a/test/production/standalone-mode/type-module/index.test.ts +++ b/test/production/standalone-mode/type-module/index.test.ts @@ -1,6 +1,7 @@ import { nextTestSetup } from 'e2e-utils' import { join } from 'path' import fs from 'fs-extra' +import cheerio from 'cheerio' import { fetchViaHTTP, findPort, @@ -22,6 +23,17 @@ describe('type-module', () => { expect(fs.existsSync(join(standalonePath, 'package.json'))).toBe(true) + // The distDir package.json acts as a commonjs boundary marker so that + // server bundles in `.next/server/**/*.js` are loaded as CJS even when + // the user's project has `"type": "module"`. Without this file, Node + // walks up to the project package.json and tries to load the server + // bundles as ESM, which fails at runtime. + const distPackageJsonPath = join(standalonePath, '.next', 'package.json') + expect(fs.existsSync(distPackageJsonPath)).toBe(true) + expect(JSON.parse(await fs.readFile(distPackageJsonPath, 'utf8'))).toEqual({ + type: 'commonjs', + }) + const serverFile = join(standalonePath, 'server.js') const appPort = await findPort() @@ -32,8 +44,17 @@ describe('type-module', () => { undefined, { cwd: next.testDir } ) - const res = await fetchViaHTTP(appPort, '/') - expect(await res.text()).toContain('hello world') + const staticRes = await fetchViaHTTP(appPort, '/') + expect(await staticRes.text()).toContain('hello world') + + // Hitting a server-rendered page forces Node to actually load + // `.next/server/pages/dynamic.js` at runtime, which only succeeds when + // the distDir commonjs boundary is in place. + const dynamicRes = await fetchViaHTTP(appPort, '/dynamic') + expect(dynamicRes.status).toBe(200) + const $ = cheerio.load(await dynamicRes.text()) + expect($('#content').text()).toBe('dynamic-rendered-at-runtime') + await killApp(server) }) }) diff --git a/test/production/standalone-mode/type-module/pages/dynamic.js b/test/production/standalone-mode/type-module/pages/dynamic.js new file mode 100644 index 000000000000..1e448dccebcb --- /dev/null +++ b/test/production/standalone-mode/type-module/pages/dynamic.js @@ -0,0 +1,7 @@ +export default function Dynamic({ now }) { + return

    dynamic-{now}

    +} + +export async function getServerSideProps() { + return { props: { now: 'rendered-at-runtime' } } +} diff --git a/turbopack/crates/turbo-tasks-fs/src/read_glob.rs b/turbopack/crates/turbo-tasks-fs/src/read_glob.rs index 2da900b979b2..7061ab72628c 100644 --- a/turbopack/crates/turbo-tasks-fs/src/read_glob.rs +++ b/turbopack/crates/turbo-tasks-fs/src/read_glob.rs @@ -204,7 +204,7 @@ async fn track_glob_internal( types.push(path.get_type()) } } - // The most likely case of this is actually a sylink resolution error, it is + // The most likely case of this is actually a symlink resolution error, it is // fine to ignore since the mere act of attempting to resolve it has triggered // the ncecessary dependencies. If this file is actually a dependency we should // get an error in the actual webpack loader when it reads it. diff --git a/turbopack/crates/turbopack-browser/src/react_refresh.rs b/turbopack/crates/turbopack-browser/src/react_refresh.rs index 222ad53007dd..45c332580b0a 100644 --- a/turbopack/crates/turbopack-browser/src/react_refresh.rs +++ b/turbopack/crates/turbopack-browser/src/react_refresh.rs @@ -63,9 +63,9 @@ pub async fn assert_can_resolve_react_refresh( request, resolve_options, ) - .first_source(); + .await?; - if result.await?.is_some() { + if result.first_source().is_some() { return Ok(ResolveReactRefreshResult::Found(request.to_resolved().await?).cell()); } } diff --git a/turbopack/crates/turbopack-cli-utils/src/runtime_entry.rs b/turbopack/crates/turbopack-cli-utils/src/runtime_entry.rs index d65afb1278fa..79f671128713 100644 --- a/turbopack/crates/turbopack-cli-utils/src/runtime_entry.rs +++ b/turbopack/crates/turbopack-cli-utils/src/runtime_entry.rs @@ -40,7 +40,6 @@ impl RuntimeEntry { None, ResolveErrorMode::Error, ) - .to_resolved() .await? .primary_modules() .await?; diff --git a/turbopack/crates/turbopack-cli/src/build/mod.rs b/turbopack/crates/turbopack-cli/src/build/mod.rs index ad57d747f34f..01467e051f5f 100644 --- a/turbopack/crates/turbopack-cli/src/build/mod.rs +++ b/turbopack/crates/turbopack-cli/src/build/mod.rs @@ -295,6 +295,7 @@ async fn build_internal( origin .resolve_asset(request_vc, origin.resolve_options(), ty) .await? + .await? .first_module() .await? .with_context(|| { diff --git a/turbopack/crates/turbopack-cli/src/dev/web_entry_source.rs b/turbopack/crates/turbopack-cli/src/dev/web_entry_source.rs index a8c8b55bff7f..14322defeb2b 100644 --- a/turbopack/crates/turbopack-cli/src/dev/web_entry_source.rs +++ b/turbopack/crates/turbopack-cli/src/dev/web_entry_source.rs @@ -138,15 +138,12 @@ pub async fn create_web_entry_source( .into_iter() .map(|request| async move { let ty = ReferenceType::Entry(EntryReferenceSubType::Web); - Ok(origin + origin .resolve_asset(request, origin.resolve_options(), ty) .await? - .to_resolved() .await? - .primary_modules() - .await? - .first() - .copied()) + .first_module() + .await }) .try_flat_join() .await?; diff --git a/turbopack/crates/turbopack-core/src/chunk/chunk_group.rs b/turbopack/crates/turbopack-core/src/chunk/chunk_group.rs index b8f43e71ff2c..e2607f80d17c 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunk_group.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunk_group.rs @@ -127,13 +127,26 @@ pub async fn make_chunk_group( }) .try_join() .await?; - let async_loader_chunk_items = async_loaders.iter().map(|&chunk_item| { - ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(ChunkItemWithAsyncModuleInfo { - chunk_item, - module: None, - async_info: None, + let async_loader_chunk_items = async_loaders + .iter() + .map(async |&chunk_item| { + let chunk_type = chunk_item + .into_trait_ref() + .await? + .ty() + .to_resolved() + .await?; + Ok(ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem( + ChunkItemWithAsyncModuleInfo { + chunk_item, + chunk_type, + module: None, + async_info: None, + }, + )) }) - }); + .try_join() + .await?; let referenced_output_assets = traced_modules .into_iter() diff --git a/turbopack/crates/turbopack-core/src/chunk/chunk_item_batch.rs b/turbopack/crates/turbopack-core/src/chunk/chunk_item_batch.rs index 8f3d92ba4b21..119b4d18b52e 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunk_item_batch.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunk_item_batch.rs @@ -11,7 +11,7 @@ use turbo_tasks::{ }; use crate::{ - chunk::{ChunkItem, ChunkItemWithAsyncModuleInfo, ChunkType, ChunkableModule, ChunkingContext}, + chunk::{ChunkItemWithAsyncModuleInfo, ChunkType, ChunkableModule, ChunkingContext}, module_graph::{ ModuleGraph, async_module_info::AsyncModulesInfo, @@ -46,8 +46,15 @@ pub async fn attach_async_info_to_chunkable_module( .as_chunk_item(module_graph, chunking_context) .to_resolved() .await?; + let chunk_type = chunk_item + .into_trait_ref() + .await? + .ty() + .to_resolved() + .await?; Ok(ChunkItemWithAsyncModuleInfo { chunk_item, + chunk_type, module: Some(module), async_info, }) @@ -103,10 +110,9 @@ impl ChunkItemOrBatchWithAsyncModuleInfo { &self, ) -> Result { Ok(match self { - Self::ChunkItem(item) => Either::Left(smallvec![( - item.chunk_item.ty().to_resolved().await?, - Self::ChunkItem(item.clone()) - )]), + Self::ChunkItem(item) => { + Either::Left(smallvec![(item.chunk_type, Self::ChunkItem(*item))]) + } Self::Batch(batch) => Either::Right(batch.split_by_chunk_type().await?), }) } @@ -167,17 +173,15 @@ impl ChunkItemBatchWithAsyncModuleInfo { let Some((_, first)) = iter.next() else { return Ok(Vc::cell(SmallVec::new())); }; - let chunk_type = first.chunk_item.ty().to_resolved().await?; - while let Some((i, item)) = iter.next() { - let ty = item.chunk_item.ty().to_resolved().await?; + let chunk_type = first.chunk_type; + for (i, item) in iter.by_ref() { + let ty = item.chunk_type; if ty != chunk_type { let mut map = FxIndexMap::default(); map.insert(chunk_type, this.chunk_items[..i].to_vec()); - map.insert(ty, vec![item.clone()]); + map.insert(ty, vec![*item]); for (_, item) in iter { - map.entry(item.chunk_item.ty().to_resolved().await?) - .or_default() - .push(item.clone()); + map.entry(item.chunk_type).or_default().push(*item); } return Ok(Vc::cell( map.into_iter() diff --git a/turbopack/crates/turbopack-core/src/chunk/chunking/dev.rs b/turbopack/crates/turbopack-core/src/chunk/chunking/dev.rs index b023399f6d5a..0c9e0c3e4af7 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunking/dev.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunking/dev.rs @@ -60,7 +60,7 @@ pub async fn expand_batches( ); let asset_ident = item.chunk_item.asset_ident().to_string(); Ok(ChunkItemOrBatchWithInfo::ChunkItem { - chunk_item: item.clone(), + chunk_item: *item, size: *size.await?, asset_ident: asset_ident.owned().await?, }) diff --git a/turbopack/crates/turbopack-core/src/chunk/chunking/mod.rs b/turbopack/crates/turbopack-core/src/chunk/chunking/mod.rs index b08d1c54199d..3da412942ac7 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunking/mod.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunking/mod.rs @@ -84,6 +84,7 @@ async fn batch_size( .map( |&ChunkItemWithAsyncModuleInfo { chunk_item, + chunk_type: _, async_info, module: _, }| { @@ -106,18 +107,21 @@ async fn plain_chunk_items_with_info( ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(chunk_item_with_info) => { let ChunkItemWithAsyncModuleInfo { chunk_item, + chunk_type, async_info, module: _, } = chunk_item_with_info; let asset_ident = chunk_item.asset_ident().to_string(); - let ty = chunk_item.ty(); - let chunk_item_size = - ty.chunk_item_size(chunking_context, *chunk_item, async_info.map(|info| *info)); + let chunk_item_size = chunk_type.chunk_item_size( + chunking_context, + *chunk_item, + async_info.map(|info| *info), + ); ChunkItemsWithInfo { by_type: smallvec![( - ty.to_resolved().await?, + chunk_type, smallvec![ChunkItemOrBatchWithInfo::ChunkItem { chunk_item: chunk_item_with_info, size: *chunk_item_size.await?, @@ -162,6 +166,7 @@ async fn plain_chunk_items_with_info_with_type( ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(chunk_item_with_info) => { let &ChunkItemWithAsyncModuleInfo { chunk_item, + chunk_type: _, async_info, module: _, } = chunk_item_with_info; @@ -172,7 +177,7 @@ async fn plain_chunk_items_with_info_with_type( Ok(( ty, smallvec![ChunkItemOrBatchWithInfo::ChunkItem { - chunk_item: chunk_item_with_info.clone(), + chunk_item: *chunk_item_with_info, size: *chunk_item_size.await?, asset_ident: asset_ident.owned().await?, }], @@ -413,7 +418,7 @@ async fn make_chunk( .into_iter() .map(|item| match item { ChunkItemOrBatchWithInfo::ChunkItem { chunk_item, .. } => { - ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(chunk_item.clone()) + ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(*chunk_item) } &ChunkItemOrBatchWithInfo::Batch { batch, .. } => { ChunkItemOrBatchWithAsyncModuleInfo::Batch(batch) diff --git a/turbopack/crates/turbopack-core/src/chunk/chunking/style_production.rs b/turbopack/crates/turbopack-core/src/chunk/chunking/style_production.rs index c70d75649e2e..310a32cb9f27 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunking/style_production.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunking/style_production.rs @@ -48,7 +48,7 @@ pub async fn make_style_production_chunks( } else { make_chunk( vec![&ChunkItemOrBatchWithInfo::ChunkItem { - chunk_item: chunk_item.clone(), + chunk_item: *chunk_item, size: 0, asset_ident: rcstr!(""), }], diff --git a/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs b/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs index 2a4c5a361364..1eebf0a2de47 100644 --- a/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs +++ b/turbopack/crates/turbopack-core/src/chunk/chunking_context.rs @@ -160,13 +160,13 @@ impl ChunkGroupResult { #[turbo_tasks::value_impl] impl ChunkGroupResult { #[turbo_tasks::function] - pub async fn output_assets_with_referenced(&self) -> Result> { - Ok(OutputAssetsWithReferenced { + pub fn output_assets_with_referenced(&self) -> Vc { + OutputAssetsWithReferenced { assets: self.assets, referenced_assets: self.referenced_assets, references: self.references, } - .cell()) + .cell() } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-core/src/chunk/mod.rs b/turbopack/crates/turbopack-core/src/chunk/mod.rs index f456940c2536..856b1a121b9e 100644 --- a/turbopack/crates/turbopack-core/src/chunk/mod.rs +++ b/turbopack/crates/turbopack-core/src/chunk/mod.rs @@ -8,7 +8,7 @@ pub(crate) mod chunking_context; pub(crate) mod data; pub(crate) mod evaluate; -use std::fmt::Display; +use std::{fmt::Display, hash::Hash}; use anyhow::{Result, bail}; use auto_hash_map::AutoSet; @@ -453,16 +453,14 @@ pub trait ChunkItem: OutputAssetsReference { } /// The type of chunk this item should be assembled into. - #[turbo_tasks::function] - fn ty(self: Vc) -> Vc>; + fn ty(&self) -> Vc>; /// A temporary method to retrieve the module associated with this /// ChunkItem. TODO: Remove this as part of the chunk refactoring. #[turbo_tasks::function] fn module(self: Vc) -> Vc>; - #[turbo_tasks::function] - fn chunking_context(self: Vc) -> Vc>; + fn chunking_context(&self) -> Vc>; } #[turbo_tasks::value_trait] @@ -514,10 +512,11 @@ impl AsyncModuleInfo { } #[derive( - Debug, Clone, PartialEq, Eq, Hash, TraceRawVcs, TaskInput, NonLocalValue, Encode, Decode, + Debug, Clone, Copy, PartialEq, Eq, Hash, TraceRawVcs, TaskInput, NonLocalValue, Encode, Decode, )] pub struct ChunkItemWithAsyncModuleInfo { pub chunk_item: ResolvedVc>, + pub chunk_type: ResolvedVc>, pub module: Option>>, pub async_info: Option>, } @@ -535,6 +534,8 @@ where async fn id(self: Vc) -> Result { let chunk_item = Vc::upcast_non_strict(self); chunk_item + .into_trait_ref() + .await? .chunking_context() .chunk_item_id_strategy() .await? diff --git a/turbopack/crates/turbopack-core/src/introspect/utils.rs b/turbopack/crates/turbopack-core/src/introspect/utils.rs index 6df3d480d827..f046a25f742a 100644 --- a/turbopack/crates/turbopack-core/src/introspect/utils.rs +++ b/turbopack/crates/turbopack-core/src/introspect/utils.rs @@ -86,7 +86,6 @@ pub async fn children_from_module_references( for &module in reference .resolve_reference() - .to_resolved() .await? .primary_modules() .await? diff --git a/turbopack/crates/turbopack-core/src/module_graph/style_groups.rs b/turbopack/crates/turbopack-core/src/module_graph/style_groups.rs index c938493b83a3..d70f6ae59b4e 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/style_groups.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/style_groups.rs @@ -12,7 +12,7 @@ use turbo_tasks::{ use crate::{ chunk::{ - ChunkItem, ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType, + ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType, ChunkableModule, ChunkingContext, chunk_item_batch::attach_async_info_to_chunkable_module, }, module::{Module, StyleModule, StyleType}, @@ -232,8 +232,8 @@ pub async fn compute_style_groups( chunking_context, ) .await?; - let ty = chunk_item.chunk_item.ty(); - let size = *ty + let size = *chunk_item + .chunk_type .chunk_item_size(chunking_context, *chunk_item.chunk_item, None) .await?; Ok((chunk_item, size)) @@ -271,7 +271,7 @@ pub async fn compute_style_groups( // The list of modules and chunk items that go into the new chunk let mut new_chunk_modules = [module].into_iter().collect::>(); - let mut new_chunk_items = vec![info.chunk_item.as_ref().unwrap().clone()]; + let mut new_chunk_items = vec![info.chunk_item.unwrap()]; // The current size of the new chunk let mut current_size = info.size; @@ -374,7 +374,7 @@ pub async fn compute_style_groups( } } - new_chunk_items.push(info.chunk_item.as_ref().unwrap().clone()); + new_chunk_items.push(info.chunk_item.unwrap()); new_chunk_modules.insert(module); *ordered_modules_with_state.get_mut(&module).unwrap() = true; continue 'outer; diff --git a/turbopack/crates/turbopack-core/src/reference/mod.rs b/turbopack/crates/turbopack-core/src/reference/mod.rs index 9517443229ea..94084c36bacd 100644 --- a/turbopack/crates/turbopack-core/src/reference/mod.rs +++ b/turbopack/crates/turbopack-core/src/reference/mod.rs @@ -178,15 +178,7 @@ pub async fn primary_referenced_modules(module: Vc>) -> Result Result> { Ok(match result.await { Ok(result_ref) => { - if result_ref.is_unresolvable_ref() { + if result_ref.is_unresolvable() { emit_unresolvable_issue( error_mode, origin, @@ -71,7 +71,7 @@ pub async fn handle_resolve_source_error( ) -> Result> { Ok(match result.await { Ok(result_ref) => { - if result_ref.is_unresolvable_ref() { + if result_ref.is_unresolvable() { emit_unresolvable_issue( error_mode, origin, diff --git a/turbopack/crates/turbopack-core/src/resolve/mod.rs b/turbopack/crates/turbopack-core/src/resolve/mod.rs index 0944d72720a8..8826462050ec 100644 --- a/turbopack/crates/turbopack-core/src/resolve/mod.rs +++ b/turbopack/crates/turbopack-core/src/resolve/mod.rs @@ -17,8 +17,8 @@ use tracing::{Instrument, Level}; use turbo_frozenmap::{FrozenMap, FrozenSet}; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ - FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryFlatJoinIterExt, - TryJoinIterExt, ValueToString, ValueToStringRef, Vc, trace::TraceRawVcs, + FxIndexMap, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, + ValueToString, ValueToStringRef, Vc, trace::TraceRawVcs, }; use turbo_tasks_fs::{FileSystemEntryType, FileSystemPath}; use turbo_unix_path::normalize_request; @@ -31,7 +31,7 @@ use crate::{ Issue, IssueExt, IssueSource, module::emit_unknown_module_type_error, resolve::ResolvingIssue, }, - module::{Module, Modules, OptionModule}, + module::Module, package_json::{PackageJsonIssue, read_package_json}, raw_module::RawModule, reference_type::ReferenceType, @@ -49,7 +49,7 @@ use crate::{ plugin::{AfterResolvePlugin, AfterResolvePluginCondition, BeforeResolvePlugin}, remap::{ExportsField, ImportsField, ReplacedSubpathValueResult}, }, - source::{OptionSource, Source, Sources}, + source::Source, }; mod alias_map; @@ -83,7 +83,7 @@ pub enum ResolveErrorMode { /// Type alias for a resolved after-resolve plugin paired with its condition. type AfterResolvePluginWithCondition = ( ResolvedVc>, - ResolvedVc, + ReadRef, ); #[turbo_tasks::value(shared)] @@ -104,9 +104,19 @@ pub enum ModuleResolveResultItem { /// Resolve the reference to an empty module. Empty, Custom(u8), + /// A duplicate of an item that appeared earlier in the primary array. + /// The usize is the index of the first occurrence. Most callers should skip + /// this variant. + /// + /// Bakes duplicate detection into the datastructure to make filtering for uniques trivial which + /// is required by primary_modules. + Duplicate(usize), } impl ModuleResolveResultItem { + // Returns the module for this item if it is one + // NOTE: if this is a `ModuleResolveResultItem::Duplicate` we return `None`, it is expected that + // callers will have already found the module earlier. async fn as_module(&self) -> Result>>> { Ok(match *self { ModuleResolveResultItem::Module(module) => Some(module), @@ -243,11 +253,13 @@ impl ModuleResolveResult { pub fn modules( modules: impl IntoIterator>)>, ) -> ResolvedVc { + let mut primary: Vec<_> = modules + .into_iter() + .map(|(k, v)| (k, ModuleResolveResultItem::Module(v))) + .collect(); + Self::mark_duplicates(&mut primary); ModuleResolveResult { - primary: modules - .into_iter() - .map(|(k, v)| (k, ModuleResolveResultItem::Module(v))) - .collect(), + primary: primary.into_boxed_slice(), affecting_sources: Default::default(), } .resolved_cell() @@ -257,11 +269,13 @@ impl ModuleResolveResult { modules: impl IntoIterator>)>, affecting_sources: Vec>>, ) -> ResolvedVc { + let mut primary: Vec<_> = modules + .into_iter() + .map(|(k, v)| (k, ModuleResolveResultItem::Module(v))) + .collect(); + Self::mark_duplicates(&mut primary); ModuleResolveResult { - primary: modules - .into_iter() - .map(|(k, v)| (k, ModuleResolveResultItem::Module(v))) - .collect(), + primary: primary.into_boxed_slice(), affecting_sources: affecting_sources.into_boxed_slice(), } .resolved_cell() @@ -269,6 +283,26 @@ impl ModuleResolveResult { } impl ModuleResolveResult { + /// Marks duplicate items as `Duplicate(first_index)` in place. + /// Preserves ordering; the first occurrence stays, subsequent occurrences + /// of the same module/output asset become `Duplicate`. + fn mark_duplicates(primary: &mut [(RequestKey, ModuleResolveResultItem)]) { + if primary.len() <= 1 { + return; + } + // Map from module identity to the index of first occurrence + let mut seen_modules = FxHashMap::default(); + for (i, (_, item)) in primary.iter_mut().enumerate() { + if let ModuleResolveResultItem::Module(m) = *item { + if let Some(&first) = seen_modules.get(&m) { + *item = ModuleResolveResultItem::Duplicate(first); + } else { + seen_modules.insert(m, i); + } + } + } + } + /// Returns all module results (but ignoring any errors). pub fn primary_modules_raw_iter( &self, @@ -279,22 +313,32 @@ impl ModuleResolveResult { }) } - /// Returns a set (no duplicates) of primary modules in the result. - pub async fn primary_modules_ref(&self) -> Result>>> { - let mut set = FxIndexSet::default(); + /// Returns primary modules (no duplicates). Emits errors for Unknown items. + /// Duplicates are already marked at construction time so no extra dedup is + /// needed here. + pub async fn primary_modules(&self) -> Result>>> { + self.primary + .iter() + .map(async |(_, item)| item.as_module().await) + .try_flat_join() + .await + } + + /// Returns the first module in the result, or None. + pub async fn first_module(&self) -> Result>>> { for (_, item) in self.primary.iter() { if let Some(module) = item.as_module().await? { - set.insert(module); + return Ok(Some(module)); } } - Ok(set.into_iter().collect()) + Ok(None) } pub fn affecting_sources_iter(&self) -> impl Iterator>> + '_ { self.affecting_sources.iter().copied() } - pub fn is_unresolvable_ref(&self) -> bool { + pub fn is_unresolvable(&self) -> bool { self.primary.is_empty() } @@ -313,25 +357,56 @@ pub struct ModuleResolveResultBuilder { impl From for ModuleResolveResult { fn from(v: ModuleResolveResultBuilder) -> Self { + let mut primary: Vec<_> = v.primary.into_iter().collect(); + Self::mark_duplicates(&mut primary); ModuleResolveResult { - primary: v.primary.into_iter().collect(), + primary: primary.into_boxed_slice(), affecting_sources: v.affecting_sources.into_boxed_slice(), } } } + +/// Resolves a `Duplicate(i)` marker by looking up the underlying item in `source`. +/// `mark_duplicates` only ever produces backwards-pointing `Duplicate` indices into +/// `Module(_)` entries, so a single lookup is enough. +fn expand_duplicate<'a>( + source: &'a [(RequestKey, ModuleResolveResultItem)], + item: &'a ModuleResolveResultItem, +) -> &'a ModuleResolveResultItem { + if let ModuleResolveResultItem::Duplicate(i) = *item { + &source[i].1 + } else { + item + } +} + impl From for ModuleResolveResultBuilder { fn from(v: ModuleResolveResult) -> Self { + // Expand `Duplicate(i)` markers as we copy into the builder. The indices are valid + // for `v.primary`, but the builder's `FxIndexMap` may be re-keyed and merged with + // other results, so the indices wouldn't survive. The final + // `From for ModuleResolveResult` re-runs `mark_duplicates` on the merged + // primary array. + let primary = v + .primary + .iter() + .map(|(k, item)| (k.clone(), expand_duplicate(&v.primary, item).clone())) + .collect(); ModuleResolveResultBuilder { - primary: IntoIterator::into_iter(v.primary).collect(), + primary, affecting_sources: v.affecting_sources.into_vec(), } } } impl ModuleResolveResultBuilder { pub fn merge_alternatives(&mut self, other: &ModuleResolveResult) { + // Expand `Duplicate(i)` markers from `other` against `other.primary` before + // inserting — the indices only make sense within `other`, not within the merged + // result. The final `mark_duplicates` pass on conversion will re-derive markers. for (k, v) in other.primary.iter() { if !self.primary.contains_key(k) { - self.primary.insert(k.clone(), v.clone()); + self.primary + .insert(k.clone(), expand_duplicate(&other.primary, v).clone()); } } let set = self @@ -369,34 +444,6 @@ impl ModuleResolveResult { Ok(*ModuleResolveResult::unresolvable()) } } - - #[turbo_tasks::function] - pub fn is_unresolvable(&self) -> Vc { - Vc::cell(self.is_unresolvable_ref()) - } - - #[turbo_tasks::function] - pub async fn first_module(&self) -> Result> { - for (_, item) in self.primary.iter() { - if let Some(module) = item.as_module().await? { - return Ok(Vc::cell(Some(module))); - } - } - Ok(Vc::cell(None)) - } - - /// Returns a set (no duplicates) of primary modules in the result. All - /// modules are already resolved Vc. - #[turbo_tasks::function] - pub async fn primary_modules(&self) -> Result> { - let mut set = FxIndexSet::default(); - for (_, item) in self.primary.iter() { - if let Some(module) = item.as_module().await? { - set.insert(module); - } - } - Ok(Vc::cell(set.into_iter().collect())) - } } #[derive( @@ -542,7 +589,7 @@ impl ValueToString for ResolveResult { #[turbo_tasks::function] async fn to_string(&self) -> Result> { let mut result = String::new(); - if self.is_unresolvable_ref() { + if self.is_unresolvable() { result.push_str("unresolvable"); } for (i, (request, item)) in self.primary.iter().enumerate() { @@ -677,10 +724,30 @@ impl ResolveResult { self.affecting_sources.iter().copied() } - pub fn is_unresolvable_ref(&self) -> bool { + pub fn is_unresolvable(&self) -> bool { self.primary.is_empty() } + pub fn first_source(&self) -> Option>> { + self.primary.iter().find_map(|(_, item)| { + if let &ResolveResultItem::Source(a) = item { + Some(a) + } else { + None + } + }) + } + + pub fn primary_sources(&self) -> impl Iterator>> { + self.primary.iter().filter_map(|(_, item)| { + if let &ResolveResultItem::Source(a) = item { + Some(a) + } else { + None + } + }) + } + pub async fn map_module(&self, source_fn: A) -> Result where A: Fn(ResolvedVc>) -> AF, @@ -921,38 +988,6 @@ impl ResolveResult { } } - #[turbo_tasks::function] - pub fn is_unresolvable(&self) -> Vc { - Vc::cell(self.is_unresolvable_ref()) - } - - #[turbo_tasks::function] - pub fn first_source(&self) -> Vc { - Vc::cell(self.primary.iter().find_map(|(_, item)| { - if let &ResolveResultItem::Source(a) = item { - Some(a) - } else { - None - } - })) - } - - #[turbo_tasks::function] - pub fn primary_sources(&self) -> Vc { - Vc::cell( - self.primary - .iter() - .filter_map(|(_, item)| { - if let &ResolveResultItem::Source(a) = item { - Some(a) - } else { - None - } - }) - .collect(), - ) - } - /// Returns a new [ResolveResult] where all [RequestKey]s are updated. The `old_request_key` /// (prefix) is replaced with the `request_key`. It's not expected that the [ResolveResult] /// contains [RequestKey]s that don't have the `old_request_key` prefix, but if there are still @@ -1648,7 +1683,7 @@ pub async fn resolve_inline( #[turbo_tasks::function] pub async fn url_resolve( origin: Vc>, - request: Vc, + request: ResolvedVc, reference_type: ReferenceType, issue_source: Option, error_mode: ResolveErrorMode, @@ -1663,11 +1698,11 @@ pub async fn url_resolve( resolve_options, ); let result = - if *rel_result.is_unresolvable().await? && *rel_request.to_resolved().await? != request { + if rel_result.await?.is_unresolvable() && rel_request.to_resolved().await? != request { let result = resolve( origin_path_parent, reference_type.clone(), - request, + *request, resolve_options, ); if resolve_options.await?.collect_affecting_sources { @@ -1691,7 +1726,7 @@ pub async fn url_resolve( result, reference_type, origin, - request, + *request, resolve_options, error_mode, issue_source, @@ -1707,13 +1742,28 @@ async fn get_matching_before_resolve_plugins( options: Vc, request: Vc, ) -> Result> { - let mut matching_plugins = Vec::new(); - for &plugin in &options.await?.before_resolve_plugins { - let condition = plugin.before_resolve_condition().to_resolved().await?; - if *condition.matches(request).await? { - matching_plugins.push(plugin); - } - } + let request_ref = request.await?; + let matching_plugins = options + .await? + .before_resolve_plugins + .iter() + .map(async |plugin| { + Ok( + if plugin + .into_trait_ref() + .await? + .before_resolve_condition() + .await? + .matches(&request_ref) + { + Some(*plugin) + } else { + None + }, + ) + }) + .try_flat_join() + .await?; Ok(Vc::cell(matching_plugins)) } @@ -1750,7 +1800,10 @@ async fn handle_after_resolve_plugins( let resolved_conditions = options_value .after_resolve_plugins .iter() - .map(async |p| Ok((*p, p.after_resolve_condition().to_resolved().await?))) + .map(async |p| { + let condition = p.into_trait_ref().await?.after_resolve_condition().await?; + Ok((*p, condition)) + }) .try_join() .await?; @@ -1762,7 +1815,7 @@ async fn handle_after_resolve_plugins( plugins_with_conditions: &[AfterResolvePluginWithCondition], ) -> Result>> { for (plugin, after_resolve_condition) in plugins_with_conditions { - if *after_resolve_condition.matches(path.clone()).await? + if after_resolve_condition.matches(&path) && let Some(result) = *plugin .after_resolve( path.clone(), @@ -2144,7 +2197,7 @@ async fn resolve_internal_inline( if !matches!(*request_value, Request::Alternatives { .. }) { // Apply fallback import mappings if provided if let Some(import_map) = &options_value.fallback_import_map - && *result.is_unresolvable().await? + && result.await?.is_unresolvable() { let result = import_map .await? @@ -2221,7 +2274,7 @@ async fn resolve_into_folder( .await?; // we are not that strict when a main field fails to resolve // we continue to try other alternatives - if !result.is_unresolvable_ref() { + if !result.is_unresolvable() { let mut result: ResolveResultBuilder = result.with_request_ref(rcstr!(".")).into(); if options_value.collect_affecting_sources { @@ -2762,7 +2815,7 @@ async fn resolve_module_request( fragment.clone(), options, ); - if !(*result.is_unresolvable().await?) { + if !result.await?.is_unresolvable() { return Ok(result); } } @@ -2997,7 +3050,7 @@ async fn resolve_import_map_result( }, ) .await? - .is_unresolvable_ref(); + .is_unresolvable(); if is_external_resolvable { Some(ResolveResultOrCell::Value(ResolveResult::primary( ResolveResultItem::External { @@ -3061,12 +3114,12 @@ impl ResolveResultOrCell { async fn into_cell_if_resolvable(self) -> Result>> { match self { ResolveResultOrCell::Cell(resolved_result) => { - if !*resolved_result.is_unresolvable().await? { + if !resolved_result.await?.is_unresolvable() { return Ok(Some(resolved_result)); } } ResolveResultOrCell::Value(resolve_result) => { - if !resolve_result.is_unresolvable_ref() { + if !resolve_result.is_unresolvable() { return Ok(Some(resolve_result.cell())); } } @@ -3411,17 +3464,23 @@ mod tests { io::Write, }; + use anyhow::Result; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{TryJoinIterExt, Vc}; use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; - use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath}; + use turbo_tasks_fs::{DiskFileSystem, FileContent, FileSystem, FileSystemPath}; use crate::{ + asset::AssetContent, + module::Module, + raw_module::RawModule, resolve::{ + ModuleResolveResult, ModuleResolveResultBuilder, ModuleResolveResultItem, RequestKey, ResolveResult, ResolveResultItem, node::node_esm_resolve_options, parse::Request, pattern::Pattern, }, source::Source, + virtual_source::VirtualSource, }; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -3709,7 +3768,7 @@ mod tests { enable_typescript_with_output_extension: bool, fully_specified: bool, custom_extensions: Option>, - ) -> anyhow::Result> { + ) -> Result> { let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)); let lookup_path = fs.root().owned().await?; @@ -3766,7 +3825,7 @@ mod tests { enable_typescript_with_output_extension: bool, fully_specified: bool, custom_extensions: Option>, - ) -> anyhow::Result> { + ) -> Result> { let request = Request::parse(pattern.clone()); let extensions = custom_extensions @@ -3801,4 +3860,221 @@ mod tests { r => panic!("request should be relative, got {r:?}"), } } + + /// Snapshot of a `ModuleResolveResult::primary` array, encoded as `Vec` so it + /// can cross the strongly-consistent read boundary (operation outputs need to be + /// `Encode`/`Decode`). One string per entry: + /// - `module:` for `Module(_)` + /// - `dup:` for `Duplicate(i)` + /// - `other` for everything else + #[turbo_tasks::value(transparent)] + pub struct DupCheckResult(Vec); + + async fn snapshot_primary(result: &ModuleResolveResult) -> Result> { + let mut out = Vec::with_capacity(result.primary.len()); + for (_, item) in result.primary.iter() { + out.push(match *item { + ModuleResolveResultItem::Module(m) => { + let ident = m.ident().await?; + format!("module:{}", ident.path.path) + } + ModuleResolveResultItem::Duplicate(i) => format!("dup:{i}"), + _ => "other".to_string(), + }); + } + Ok(out) + } + + #[turbo_tasks::function] + fn fs() -> Vc> { + Vc::upcast(DiskFileSystem::new(rcstr!("temp"), Vc::cell(fs_path()))) + } + + #[turbo_tasks::function] + async fn make_module(name: RcStr) -> Result>> { + let path = fs().root().await?.join(&name)?; + let file_content = + FileContent::Content(turbo_tasks_fs::File::from(format!("// {name}"))).resolved_cell(); + let content = AssetContent::file(*file_content).to_resolved().await?; + let source = VirtualSource::new(path, *content); + let module = RawModule::new(Vc::upcast(source)).to_resolved().await?; + Ok(Vc::upcast(*module)) + } + + fn fs_path() -> RcStr { + rcstr!("/tmp/_mdt") + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn modules_constructor_marks_module_duplicates() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + #[turbo_tasks::function(operation)] + async fn run_test() -> Result> { + let m_a = make_module(rcstr!("a.js")).to_resolved().await?; + let m_b = make_module(rcstr!("b.js")).to_resolved().await?; + + let result = ModuleResolveResult::modules([ + (RequestKey::new(rcstr!("a")), m_a), + (RequestKey::new(rcstr!("b")), m_b), + (RequestKey::new(rcstr!("a-again")), m_a), + (RequestKey::new(rcstr!("b-again")), m_b), + ]) + .await?; + + // primary_modules() yields each module exactly once, in first-seen order. + let modules = result.primary_modules().await?; + assert_eq!(modules, vec![m_a, m_b]); + + Ok(Vc::cell(snapshot_primary(&result).await?)) + } + tt.run_once(async move { + let snap = run_test().read_strongly_consistent().await?; + assert_eq!( + snap.iter().map(String::as_str).collect::>(), + vec!["module:a.js", "module:b.js", "dup:0", "dup:1"] + ); + Ok(()) + }) + .await + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn first_module_returns_first_when_duplicates_follow() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + #[turbo_tasks::function(operation)] + async fn run_test() -> Result> { + let m = make_module(rcstr!("a.js")).to_resolved().await?; + + let result = ModuleResolveResult::modules([ + (RequestKey::default(), m), + (RequestKey::new(rcstr!("again")), m), + (RequestKey::new(rcstr!("once-more")), m), + ]) + .await?; + + assert_eq!(result.first_module().await?, Some(m)); + assert_eq!(result.primary_modules().await?, vec![m]); + Ok(Vc::cell(snapshot_primary(&result).await?)) + } + tt.run_once(async move { + let snap = run_test().read_strongly_consistent().await?; + assert_eq!( + snap.iter().map(String::as_str).collect::>(), + vec!["module:a.js", "dup:0", "dup:0"] + ); + Ok(()) + }) + .await + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn builder_marks_module_duplicates_skipping_non_dedup_items() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + #[turbo_tasks::function(operation)] + async fn run_test() -> Result> { + let m = make_module(rcstr!("a.js")).to_resolved().await?; + + let mut builder = ModuleResolveResultBuilder { + primary: Default::default(), + affecting_sources: Vec::new(), + }; + builder.primary.insert( + RequestKey::new(rcstr!("k0")), + ModuleResolveResultItem::Module(m), + ); + builder.primary.insert( + RequestKey::new(rcstr!("k1")), + ModuleResolveResultItem::Empty, + ); + builder.primary.insert( + RequestKey::new(rcstr!("k2")), + ModuleResolveResultItem::Module(m), + ); + let result: ModuleResolveResult = builder.into(); + assert_eq!(result.primary_modules().await?, vec![m]); + Ok(Vc::cell(snapshot_primary(&result).await?)) + } + tt.run_once(async move { + let snap = run_test().read_strongly_consistent().await?; + + assert_eq!( + snap.iter().map(String::as_str).collect::>(), + vec!["module:a.js", "other", "dup:0"] + ); + Ok(()) + }) + .await + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn alternatives_preserves_unique_module_set() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + #[turbo_tasks::function(operation)] + async fn run_test() -> Result> { + let m_a = make_module(rcstr!("a.js")).to_resolved().await?; + let m_b = make_module(rcstr!("b.js")).to_resolved().await?; + + // r1 has m_a twice → Module(m_a), Duplicate(0). + let r1 = *ModuleResolveResult::modules([ + (RequestKey::new(rcstr!("k1")), m_a), + (RequestKey::new(rcstr!("k2")), m_a), + ]); + // r2 prepended with m_b so the ordering inside r2 puts m_b at index 0 — a "stale" + // 0 from r1 would now incorrectly point at m_b after a naive concatenation. + let r2 = *ModuleResolveResult::module(m_b); + + let merged = ModuleResolveResult::alternatives(vec![r1, r2]).await?; + assert_eq!(merged.primary_modules().await?, vec![m_a, m_b]); + + // Verify every Duplicate(i) is well-formed + for (i, (_, item)) in merged.primary.iter().enumerate() { + if let ModuleResolveResultItem::Duplicate(first) = *item { + assert!( + first < i, + "Duplicate index {first} at position {i} must point backwards" + ); + let pointed = &merged.primary[first].1; + let ModuleResolveResultItem::Module(pointed_module) = *pointed else { + panic!( + "Duplicate({first}) at {i} points at {pointed:?}, expected a concrete \ + Module" + ); + }; + // The pointed-at module must be m_a — proves the index was re-derived + // against the merged array, not carried stale from r1. + assert_eq!( + pointed_module, m_a, + "Duplicate({first}) at position {i} points at the wrong module" + ); + } + } + Ok(Vc::cell(snapshot_primary(&merged).await?)) + } + tt.run_once(async move { + let snap = run_test().read_strongly_consistent().await?; + + assert_eq!( + snap.iter().map(String::as_str).collect::>(), + vec!["module:a.js", "dup:0", "module:b.js"] + ); + Ok(()) + }) + .await + .unwrap(); + } } diff --git a/turbopack/crates/turbopack-core/src/resolve/plugin.rs b/turbopack/crates/turbopack-core/src/resolve/plugin.rs index 1b1e7f7774d5..36bdb86bd0db 100644 --- a/turbopack/crates/turbopack-core/src/resolve/plugin.rs +++ b/turbopack/crates/turbopack-core/src/resolve/plugin.rs @@ -1,7 +1,6 @@ use anyhow::Result; -use rustc_hash::FxHashSet; use turbo_rcstr::RcStr; -use turbo_tasks::{ResolvedVc, Vc}; +use turbo_tasks::{ReadRef, ResolvedVc, Vc}; use turbo_tasks_fs::{FileSystemPath, glob::Glob}; use crate::{ @@ -10,12 +9,17 @@ use crate::{ }; /// A condition which determines if the hooks of a resolve plugin gets called. -#[turbo_tasks::value(shared)] +/// +/// The glob is read at construction time and stored as a `ReadRef`, so `matches` is a pure +/// sync function. `serialization = "skip"` because serializing a `ReadRef` is wasteful and +/// recomputing this is very cheap. +#[turbo_tasks::value(serialization = "skip")] pub enum AfterResolvePluginCondition { Glob { root: FileSystemPath, - glob: ResolvedVc, + glob: ReadRef, }, + // these variants are used by utoo Always, Never, } @@ -23,38 +27,31 @@ pub enum AfterResolvePluginCondition { #[turbo_tasks::value_impl] impl AfterResolvePluginCondition { #[turbo_tasks::function] - pub fn new_with_glob(root: FileSystemPath, glob: ResolvedVc) -> Vc { - AfterResolvePluginCondition::Glob { root, glob }.cell() + pub async fn new_with_glob(root: FileSystemPath, glob: ResolvedVc) -> Result> { + let glob = glob.await?; + Ok(AfterResolvePluginCondition::Glob { root, glob }.cell()) } } -#[turbo_tasks::value_impl] impl AfterResolvePluginCondition { - #[turbo_tasks::function] - pub async fn matches(&self, fs_path: FileSystemPath) -> Result> { + /// Test whether `fs_path` matches this condition. + pub fn matches(&self, fs_path: &FileSystemPath) -> bool { match self { AfterResolvePluginCondition::Glob { root, glob } => { - let path = fs_path; - - if let Some(path) = root.get_path_to(&path) - && glob.await?.matches(path) - { - return Ok(Vc::cell(true)); - } - - Ok(Vc::cell(false)) + root.get_path_to(fs_path).is_some_and(|p| glob.matches(p)) } - AfterResolvePluginCondition::Always => Ok(Vc::cell(true)), - AfterResolvePluginCondition::Never => Ok(Vc::cell(false)), + AfterResolvePluginCondition::Always => true, + AfterResolvePluginCondition::Never => false, } } } /// A condition which determines if the hooks of a resolve plugin gets called. -#[turbo_tasks::value(shared)] +#[turbo_tasks::value(shared, serialization = "skip")] pub enum BeforeResolvePluginCondition { - Request(ResolvedVc), - Modules(FxHashSet), + Request(ReadRef), + Modules(ReadRef>), + // These are used by utoo Always, Never, } @@ -63,26 +60,25 @@ pub enum BeforeResolvePluginCondition { impl BeforeResolvePluginCondition { #[turbo_tasks::function] pub async fn from_modules(modules: ResolvedVc>) -> Result> { - Ok(BeforeResolvePluginCondition::Modules(modules.await?.iter().cloned().collect()).cell()) + Ok(BeforeResolvePluginCondition::Modules(modules.await?).cell()) } #[turbo_tasks::function] - pub fn from_request_glob(glob: ResolvedVc) -> Vc { - BeforeResolvePluginCondition::Request(glob).cell() + pub async fn from_request_glob(glob: ResolvedVc) -> Result> { + Ok(BeforeResolvePluginCondition::Request(glob.await?).cell()) } } -#[turbo_tasks::value_impl] impl BeforeResolvePluginCondition { - #[turbo_tasks::function] - pub async fn matches(&self, request: Vc) -> Result> { - Ok(Vc::cell(match self { - BeforeResolvePluginCondition::Request(glob) => match request.await?.request() { - Some(request) => glob.await?.matches(request.as_str()), + /// Test whether `request` matches this condition. + pub fn matches(&self, request: &Request) -> bool { + match self { + BeforeResolvePluginCondition::Request(glob) => match request.request() { + Some(request) => glob.matches(request.as_str()), None => false, }, BeforeResolvePluginCondition::Modules(modules) => { - if let Request::Module { module, .. } = &*request.await? { + if let Request::Module { module, .. } = request { modules.iter().any(|m| module.is_match(m)) } else { false @@ -90,14 +86,13 @@ impl BeforeResolvePluginCondition { } BeforeResolvePluginCondition::Always => true, BeforeResolvePluginCondition::Never => false, - })) + } } } #[turbo_tasks::value_trait] pub trait BeforeResolvePlugin { - #[turbo_tasks::function] - fn before_resolve_condition(self: Vc) -> Vc; + fn before_resolve_condition(&self) -> Vc; #[turbo_tasks::function] fn before_resolve( @@ -111,8 +106,7 @@ pub trait BeforeResolvePlugin { #[turbo_tasks::value_trait] pub trait AfterResolvePlugin { /// A condition which determines if the hooks gets called. - #[turbo_tasks::function] - fn after_resolve_condition(self: Vc) -> Vc; + fn after_resolve_condition(&self) -> Vc; /// This hook gets called when a full filepath has been resolved and the /// condition matches. If a value is returned it replaces the resolve diff --git a/turbopack/crates/turbopack-css/src/asset.rs b/turbopack/crates/turbopack-css/src/asset.rs index 255fa977a165..55e43b79c028 100644 --- a/turbopack/crates/turbopack-css/src/asset.rs +++ b/turbopack/crates/turbopack-css/src/asset.rs @@ -248,12 +248,10 @@ impl ChunkItem for CssModuleChunkItem { self.module.ident() } - #[turbo_tasks::function] fn chunking_context(&self) -> Vc> { *self.chunking_context } - #[turbo_tasks::function] fn ty(&self) -> Vc> { Vc::upcast(Vc::::default()) } @@ -278,7 +276,6 @@ impl CssChunkItem for CssModuleChunkItem { { for &module in import_ref .resolve_reference() - .to_resolved() .await? .primary_modules() .await? @@ -300,7 +297,6 @@ impl CssChunkItem for CssModuleChunkItem { { for &module in compose_ref .resolve_reference() - .to_resolved() .await? .primary_modules() .await? diff --git a/turbopack/crates/turbopack-css/src/module_asset.rs b/turbopack/crates/turbopack-css/src/module_asset.rs index 6e4186860ac3..af858c19823c 100644 --- a/turbopack/crates/turbopack-css/src/module_asset.rs +++ b/turbopack/crates/turbopack-css/src/module_asset.rs @@ -284,15 +284,16 @@ impl EcmascriptChunkPlaceable for EcmascriptCssModule { original: original_name, from, } => { - let resolved_module = from.resolve_reference().first_module().await?; + let resolved_module = + from.resolve_reference().await?.first_module().await?; - let Some(resolved_module) = &*resolved_module else { + let Some(resolved_module) = resolved_module else { // Issue already emitted by CssModuleComposeReference::resolve_reference continue; }; let Some(css_module) = - ResolvedVc::try_downcast_type::(*resolved_module) + ResolvedVc::try_downcast_type::(resolved_module) else { // Issue already emitted by CssModuleComposeReference::resolve_reference continue; diff --git a/turbopack/crates/turbopack-css/src/references/compose.rs b/turbopack/crates/turbopack-css/src/references/compose.rs index 762cd15ae12c..b1a373cb298b 100644 --- a/turbopack/crates/turbopack-css/src/references/compose.rs +++ b/turbopack/crates/turbopack-css/src/references/compose.rs @@ -46,10 +46,10 @@ impl ModuleReference for CssModuleComposeReference { None, ); - let resolved = result.first_module().await?; + let resolved = result.await?.first_module().await?; let file_path = self.origin.origin_path().to_resolved().await?; - if let Some(module) = &*resolved { - if ResolvedVc::try_downcast_type::(*module).is_none() { + if let Some(module) = resolved { + if ResolvedVc::try_downcast_type::(module).is_none() { CssModuleComposesIssue { severity: IssueSeverity::Error, file_path, diff --git a/turbopack/crates/turbopack-css/src/references/url.rs b/turbopack/crates/turbopack-css/src/references/url.rs index 094b489cd3d8..9e42a362fcfa 100644 --- a/turbopack/crates/turbopack-css/src/references/url.rs +++ b/turbopack/crates/turbopack-css/src/references/url.rs @@ -58,7 +58,7 @@ impl UrlAssetReference { self: Vc, chunking_context: Vc>, ) -> Result> { - if let Some(module) = *self.resolve_reference().first_module().await? + if let Some(module) = self.resolve_reference().await?.first_module().await? && let Some(embeddable) = ResolvedVc::try_downcast::>(module) { return Ok(ReferencedAsset::Some( diff --git a/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs b/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs index b8912b2285f0..aeb42cc9ac05 100644 --- a/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs +++ b/turbopack/crates/turbopack-ecmascript/src/chunk/chunk_type.rs @@ -57,12 +57,18 @@ impl ChunkType for EcmascriptChunkType { else { bail!("Chunk item is not an ecmascript chunk item but reporting chunk type ecmascript"); }; - Ok(Vc::cell( - chunk_item - .content_with_async_module_info(async_module_info, true) - .await - .map_or(0, |content| round_chunk_item_size(content.inner_code.len())), - )) + let chunk_item = chunk_item.into_trait_ref().await?; + let size = match chunk_item + .content_with_async_module_info(async_module_info, true) + .await + { + Ok(content) => { + let content = content.await?; + round_chunk_item_size(content.inner_code.len()) + } + Err(_) => 0, + }; + Ok(Vc::cell(size)) } } diff --git a/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs b/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs index 53a68d7a985e..8fb6690bcd8e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs +++ b/turbopack/crates/turbopack-ecmascript/src/chunk/item.rs @@ -1,6 +1,7 @@ use std::io::Write; use anyhow::{Result, bail}; +use async_trait::async_trait; use bincode::{Decode, Encode}; use smallvec::SmallVec; use turbo_rcstr::{RcStr, rcstr}; @@ -240,6 +241,7 @@ impl EcmascriptChunkItemWithAsyncInfo { ) -> Result { let ChunkItemWithAsyncModuleInfo { chunk_item, + chunk_type: _, module: _, async_info, } = chunk_item; @@ -255,23 +257,18 @@ impl EcmascriptChunkItemWithAsyncInfo { } } +#[async_trait] #[turbo_tasks::value_trait] pub trait EcmascriptChunkItem: ChunkItem + OutputAssetsReference { - #[turbo_tasks::function] - fn content(self: Vc) -> Vc; - /// Fetches the content of the chunk item with async module info. /// When `estimated` is true, it's ok to provide an estimated content, since it's only used for /// compute the chunking. When `estimated` is true, this function should not invoke other /// chunking operations that would cause cycles. - #[turbo_tasks::function] - fn content_with_async_module_info( - self: Vc, - _async_module_info: Option>, - _estimated: bool, - ) -> Vc { - self.content() - } + async fn content_with_async_module_info( + &self, + async_module_info: Option>, + estimated: bool, + ) -> Result>; } pub trait EcmascriptChunkItemExt { @@ -295,13 +292,18 @@ async fn module_factory_with_code_generation_issue( chunk_item: Vc>, async_module_info: Option>, ) -> Result> { - let content = match chunk_item - .content_with_async_module_info(async_module_info, false) - .await - { - Ok(item) => item.module_factory().await, - Err(err) => Err(err), - }; + async fn get_content( + chunk_item: Vc>, + async_module_info: Option>, + ) -> Result> { + let chunk_item_ref = chunk_item.into_trait_ref().await?; + let content = chunk_item_ref + .content_with_async_module_info(async_module_info, false) + .await? + .await?; + content.module_factory().await + } + let content = get_content(chunk_item, async_module_info).await; Ok(match content { Ok(factory) => *factory, Err(error) => { @@ -372,7 +374,6 @@ impl ChunkItem for EcmascriptModuleChunkItem { .chunk_item_content_ident(*self.chunking_context, *self.module_graph) } - #[turbo_tasks::function] fn ty(&self) -> Vc> { Vc::upcast(Vc::::default()) } @@ -382,7 +383,6 @@ impl ChunkItem for EcmascriptModuleChunkItem { Vc::upcast(*self.module) } - #[turbo_tasks::function] fn chunking_context(&self) -> Vc> { *self.chunking_context } @@ -397,25 +397,19 @@ impl OutputAssetsReference for EcmascriptModuleChunkItem { } } +#[async_trait] #[turbo_tasks::value_impl] impl EcmascriptChunkItem for EcmascriptModuleChunkItem { - #[turbo_tasks::function] - fn content(&self) -> Vc { - self.module - .chunk_item_content(*self.chunking_context, *self.module_graph, None, false) - } - - #[turbo_tasks::function] - fn content_with_async_module_info( + async fn content_with_async_module_info( &self, async_module_info: Option>, estimated: bool, - ) -> Vc { - self.module.chunk_item_content( + ) -> Result> { + Ok(self.module.chunk_item_content( *self.chunking_context, *self.module_graph, async_module_info, estimated, - ) + )) } } diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index 8c2c51d74878..c0eb5f227ea4 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -778,11 +778,6 @@ impl EcmascriptModuleAsset { *self.source } - #[turbo_tasks::function] - pub fn analyze(self: Vc) -> Vc { - analyze_ecmascript_module(self, None) - } - #[turbo_tasks::function] pub fn options(&self) -> Vc { *self.options @@ -790,6 +785,10 @@ impl EcmascriptModuleAsset { } impl EcmascriptModuleAsset { + pub fn analyze(self: Vc) -> Vc { + analyze_ecmascript_module(self, None) + } + pub async fn parse(&self) -> Result> { let options = self.options.await?; let node_env = self diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index b9b0df0d8b32..03d5e443d16b 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -331,7 +331,7 @@ impl ReferencedAsset { pub async fn from_resolve_result(resolve_result: Vc) -> Result { // TODO handle multiple keyed results let result = resolve_result.await?; - if result.is_unresolvable_ref() { + if result.is_unresolvable() { return Ok(ReferencedAsset::Unresolvable); } for (_, result) in result.primary.iter() { @@ -475,7 +475,7 @@ impl ModuleReference for EsmAssetReference { loader_request, origin.resolve_options(), ); - let loader_fs_path = if let Some(source) = *resolved.first_source().await? { + let loader_fs_path = if let Some(source) = resolved.await?.first_source() { source.ident().await?.path.clone() } else { bail!("Unable to resolve turbopackLoader '{}'", loader.loader); @@ -544,7 +544,7 @@ impl ModuleReference for EsmAssetReference { .await?; if let Some(ModulePart::Export(export_name)) = &self.export_name { - for &module in result.primary_modules().await? { + for &module in result.await?.primary_modules().await?.iter() { if let Some(module) = ResolvedVc::try_downcast(module) && *is_export_missing(*module, export_name.clone()).await? { diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index fe6922df9c3a..dcaff9048e72 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -3622,10 +3622,9 @@ async fn require_resolve_visitor( .await?; let mut values = resolved - .primary_sources() .await? - .iter() - .map(|&source| async move { + .primary_sources() + .map(|source| async move { Ok(require_resolve(source.ident().await?.path.clone()).into()) }) .try_join() diff --git a/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs b/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs index 198b21ada3c1..f38db658bdd4 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/pattern_mapping.rs @@ -85,7 +85,7 @@ pub(crate) enum PatternMapping { /// ```js /// require(`./images/${name}.png`) /// ``` - Map(#[bincode(with = "turbo_bincode::indexmap")] FxIndexMap), + Map(#[bincode(with = "turbo_bincode::indexmap")] FxIndexMap), } #[derive( @@ -231,7 +231,7 @@ enum ImportMode { } fn create_context_map( - map: &FxIndexMap, + map: &FxIndexMap, key_expr: &Expr, import_mode: ImportMode, ) -> Expr { @@ -304,6 +304,7 @@ async fn to_single_pattern_mapping( origin: Vc>, chunking_context: Vc>, resolve_item: &ModuleResolveResultItem, + primary: &[(turbopack_core::resolve::RequestKey, ModuleResolveResultItem)], resolve_type: ResolveType, ) -> Result { let module = match resolve_item { @@ -328,6 +329,16 @@ async fn to_single_pattern_mapping( .to_unstyled_string(), )); } + ModuleResolveResultItem::Duplicate(first) => { + return Box::pin(to_single_pattern_mapping( + origin, + chunking_context, + &primary[*first].1, + primary, + resolve_type, + )) + .await; + } ModuleResolveResultItem::Empty | ModuleResolveResultItem::Custom(_) => { // TODO implement mapping CodeGenerationIssue { @@ -405,24 +416,37 @@ impl PatternMapping { .cell()), 1 if !request.request_pattern().await?.has_dynamic_parts() => { let resolve_item = &result.primary.first().unwrap().1; - let single_pattern_mapping = - to_single_pattern_mapping(origin, chunking_context, resolve_item, resolve_type) - .await?; + let single_pattern_mapping = to_single_pattern_mapping( + origin, + chunking_context, + resolve_item, + &result.primary, + resolve_type, + ) + .await?; Ok(PatternMapping::Single(single_pattern_mapping).cell()) } _ => { + let primary = &result.primary; let mut set = HashSet::new(); - let map = result - .primary + let items: Vec<(RcStr, &ModuleResolveResultItem)> = primary .iter() .filter_map(|(k, v)| { let request = k.request.as_ref()?; - set.insert(request).then(|| (request.to_string(), v)) + set.insert(request).then(|| (request.clone(), v)) }) + .collect(); + let map = items + .into_iter() .map(|(k, v)| async move { - let single_pattern_mapping = - to_single_pattern_mapping(origin, chunking_context, v, resolve_type) - .await?; + let single_pattern_mapping = to_single_pattern_mapping( + origin, + chunking_context, + v, + primary, + resolve_type, + ) + .await?; Ok((k, single_pattern_mapping)) }) .try_join() diff --git a/turbopack/crates/turbopack-node/src/transforms/webpack.rs b/turbopack/crates/turbopack-node/src/transforms/webpack.rs index 4a4b902a8d72..5dbee39c265c 100644 --- a/turbopack/crates/turbopack-node/src/transforms/webpack.rs +++ b/turbopack/crates/turbopack-node/src/transforms/webpack.rs @@ -661,7 +661,7 @@ impl EvaluateContext for WebpackLoaderContext { options, ); - if let Some(source) = *resolved.first_source().await? { + if let Some(source) = resolved.await?.first_source() { if let Some(path) = self.cwd.get_relative_path_to(&source.ident().await?.path) { Ok(ResponseMessage::Resolve { path }) } else { @@ -702,7 +702,7 @@ impl EvaluateContext for WebpackLoaderContext { ) .await?; - let Some(module) = *resolved.first_module().await? else { + let Some(module) = resolved.await?.first_module().await? else { bail!( "importModule: unable to resolve {} in {}", request, diff --git a/turbopack/crates/turbopack-resolve/src/node_native_binding.rs b/turbopack/crates/turbopack-resolve/src/node_native_binding.rs index f2e2f874238d..7f5c26e8ab07 100644 --- a/turbopack/crates/turbopack-resolve/src/node_native_binding.rs +++ b/turbopack/crates/turbopack-resolve/src/node_native_binding.rs @@ -95,10 +95,9 @@ async fn resolve_node_pre_gyp_files( collect_affecting_sources, true, ) - .first_source() .await?; let compile_target = compile_target.await?; - if let Some(config_asset) = *config + if let Some(config_asset) = config.first_source() && let AssetContent::File(file) = &*config_asset.content().await? && let FileContent::Content(config_file) = &*file.await? { @@ -273,9 +272,11 @@ async fn resolve_node_gyp_build_files( collect_affecting_sources, true, ); - if let [binding_gyp] = &gyp_file.primary_sources().await?[..] { + let gyp_file = gyp_file.await?; + let mut primary_sources = gyp_file.primary_sources(); + if let (Some(binding_gyp), None) = (primary_sources.next(), primary_sources.next()) { let mut merged_affecting_sources = if collect_affecting_sources { - gyp_file.await?.get_affecting_sources().collect::>() + gyp_file.get_affecting_sources().collect::>() } else { Vec::new() }; @@ -399,9 +400,8 @@ async fn resolve_node_bindings_files( collect_affecting_sources, true, ) - .first_source() .await?; - if let Some(asset) = *resolved + if let Some(asset) = resolved.first_source() && let AssetContent::File(file) = &*asset.content().await? && let FileContent::Content(_) = &*file.await? { diff --git a/turbopack/crates/turbopack-resolve/src/typescript.rs b/turbopack/crates/turbopack-resolve/src/typescript.rs index 9b2a81b0f7ec..c7cd45eba5cf 100644 --- a/turbopack/crates/turbopack-resolve/src/typescript.rs +++ b/turbopack/crates/turbopack-resolve/src/typescript.rs @@ -25,7 +25,7 @@ use turbopack_core::{ pattern::Pattern, resolve, }, - source::{OptionSource, Source}, + source::Source, }; use crate::ecmascript::get_condition_maps; @@ -88,7 +88,7 @@ pub async fn read_tsconfigs( configs.push((parsed_data, tsconfig)); if let Some(extends) = json["extends"].as_str() { let resolved = resolve_extends(*tsconfig, extends, resolve_options).await?; - if let Some(source) = *resolved.await? { + if let Some(source) = resolved { data = source.content().file_content(); tsconfig = source; continue; @@ -118,7 +118,7 @@ async fn resolve_extends( tsconfig: Vc>, extends: &str, resolve_options: Vc, -) -> Result> { +) -> Result>>> { let parent_dir = tsconfig.ident().await?.path.parent(); let request = Request::parse_string(extends.into()); @@ -148,18 +148,18 @@ async fn resolve_extends( Request::Empty => { let request = Request::parse_string(rcstr!("./tsconfig")); Ok(resolve(parent_dir, - ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).first_source()) + ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).await?.first_source()) } // All other types are treated as module imports, and potentially joined with // "tsconfig.json". This includes "relative" imports like '.' and '..'. _ => { - let mut result = resolve(parent_dir.clone(), ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).first_source(); - if result.await?.is_none() { - let request = Request::parse_string(format!("{extends}/tsconfig").into()); - result = resolve(parent_dir, ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).first_source(); + let result = resolve(parent_dir.clone(), ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).await?; + if let Some(source) = result.first_source() { + return Ok(Some(source)); } - Ok(result) + let request = Request::parse_string(format!("{extends}/tsconfig").into()); + Ok(resolve(parent_dir, ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).await?.first_source()) } } } @@ -169,19 +169,21 @@ async fn resolve_extends_rooted_or_relative( request: Vc, resolve_options: Vc, path: &str, -) -> Result> { - let mut result = resolve( +) -> Result>>> { + let result = resolve( lookup_path.clone(), ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options, ) - .first_source(); + .await?; + + let mut result = result.first_source(); // If the file doesn't end with ".json" and we can't find the file, then we have // to try again with it. // https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3305 - if !path.ends_with(".json") && result.await?.is_none() { + if !path.ends_with(".json") && result.is_none() { let request = Request::parse_string(format!("{path}.json").into()); result = resolve( lookup_path.clone(), @@ -189,6 +191,7 @@ async fn resolve_extends_rooted_or_relative( request, resolve_options, ) + .await? .first_source(); } Ok(result) @@ -424,7 +427,7 @@ pub async fn type_resolve( request, options, ); - if !*result1.is_unresolvable().await? { + if !result1.await?.is_unresolvable() { result1 } else { resolve( diff --git a/turbopack/crates/turbopack/src/lib.rs b/turbopack/crates/turbopack/src/lib.rs index 300f365a86ea..15229f14b191 100644 --- a/turbopack/crates/turbopack/src/lib.rs +++ b/turbopack/crates/turbopack/src/lib.rs @@ -421,20 +421,12 @@ impl ModuleAssetContext { *self.resolve_options_context } - #[turbo_tasks::function] - pub async fn is_types_resolving_enabled(&self) -> Result> { - let resolve_options_context = self.resolve_options_context.await?; - Ok(Vc::cell( - resolve_options_context.enable_types && resolve_options_context.enable_typescript, - )) - } - #[turbo_tasks::function] pub async fn with_types_resolving_enabled(self: Vc) -> Result> { - if *self.is_types_resolving_enabled().await? { + let this = self.await?; + if this.is_types_resolving_enabled().await? { return Ok(self); } - let this = self.await?; let resolve_options_context = *this .resolve_options_context .with_types_enabled() @@ -452,6 +444,10 @@ impl ModuleAssetContext { } impl ModuleAssetContext { + async fn is_types_resolving_enabled(&self) -> Result { + let resolve_options_context = self.resolve_options_context.await?; + Ok(resolve_options_context.enable_types && resolve_options_context.enable_typescript) + } async fn process_with_transition_rules( self: Vc, source: ResolvedVc>, @@ -1025,8 +1021,8 @@ impl AssetContext for ModuleAssetContext { ); let mut result = self.process_resolve_result(*result.to_resolved().await?, reference_type); - - if *self.is_types_resolving_enabled().await? { + let this = self.await?; + if this.is_types_resolving_enabled().await? { let types_result = type_resolve( Vc::upcast(PlainResolveOrigin::new(Vc::upcast(self), origin_path)), request, diff --git a/turbopack/scripts/analyze_cache_effectiveness.py b/turbopack/scripts/analyze_cache_effectiveness.py index 47183f76bde0..6fb1e8385593 100644 --- a/turbopack/scripts/analyze_cache_effectiveness.py +++ b/turbopack/scripts/analyze_cache_effectiveness.py @@ -12,11 +12,20 @@ The JSON format contains entries like: { "task_name": { "cache_hit": N, "cache_miss": N } } + +Usage: + analyze_cache_effectiveness.py + analyze_cache_effectiveness.py --diff [--top N] + +In --diff mode, the script reports the tasks whose hits or misses changed the +most between the two runs — useful for evaluating the impact of removing or +adding `turbo_tasks` caching on specific functions. """ +import argparse import json import sys -from typing import List, Tuple +from typing import Dict, List from dataclasses import dataclass @@ -54,6 +63,10 @@ def load_task_stats(file_path: str) -> List[TaskStats]: return tasks +def load_task_stats_map(file_path: str) -> Dict[str, TaskStats]: + return {t.name: t for t in load_task_stats(file_path)} + + def analyze_tasks(tasks: List[TaskStats]) -> List[TaskStats]: """Analyze all tasks and return sorted by wasted cache overhead. @@ -114,27 +127,213 @@ def print_analysis(tasks: List[TaskStats]): print(f"Tasks with <50% hit rate: {low_hit_rate_count}") -def main(): - if len(sys.argv) != 2: - print("Usage: python analyze_cache_effectiveness.py ") - sys.exit(1) +@dataclass +class TaskDiff: + name: str + before_hit: int + after_hit: int + before_miss: int + after_miss: int + + @property + def delta_hit(self) -> int: + return self.after_hit - self.before_hit - file_path = sys.argv[1] + @property + def delta_miss(self) -> int: + return self.after_miss - self.before_miss - try: - tasks = load_task_stats(file_path) - tasks = analyze_tasks(tasks) - print_analysis(tasks) + @staticmethod + def _rate(hit: int, miss: int) -> float: + total = hit + miss + return hit / total if total > 0 else 0.0 + + @property + def before_rate(self) -> float: + return self._rate(self.before_hit, self.before_miss) + + @property + def after_rate(self) -> float: + return self._rate(self.after_hit, self.after_miss) + + @property + def delta_rate(self) -> float: + return self.after_rate - self.before_rate + + +def compute_diff( + before: Dict[str, TaskStats], after: Dict[str, TaskStats] +) -> List[TaskDiff]: + names = set(before) | set(after) + diffs = [] + zero = TaskStats(name="", cache_hit=0, cache_miss=0) + for name in names: + b = before.get(name, zero) + a = after.get(name, zero) + diffs.append( + TaskDiff( + name=name, + before_hit=b.cache_hit, + after_hit=a.cache_hit, + before_miss=b.cache_miss, + after_miss=a.cache_miss, + ) + ) + return diffs + + +def _print_diff_section( + title: str, diffs: List[TaskDiff], key, top: int, reverse: bool +): + print("=" * 100) + print(title) + print("=" * 100) + ordered = sorted(diffs, key=key, reverse=reverse) + header = ( + f"{'Δhit':>12} {'Δmiss':>12} {'hit b→a':>22} {'miss b→a':>22} " + f"{'rate b→a':>18} Task" + ) + print(header) + print("-" * len(header)) + shown = 0 + for d in ordered: + # Stop once the signed delta crosses zero in the direction we care about. + v = key(d) + if reverse and v <= 0: + break + if not reverse and v >= 0: + break + hit_str = f"{d.before_hit:,}→{d.after_hit:,}" + miss_str = f"{d.before_miss:,}→{d.after_miss:,}" + rate_str = f"{d.before_rate:.0%}→{d.after_rate:.0%}" + print( + f"{d.delta_hit:>+12,} {d.delta_miss:>+12,} " + f"{hit_str:>22} {miss_str:>22} {rate_str:>18} {d.name}" + ) + shown += 1 + if shown >= top: + break + if shown == 0: + print("(none)") + print() + + +def print_diff( + before: Dict[str, TaskStats], after: Dict[str, TaskStats], top: int +): + diffs = compute_diff(before, after) + + before_hits = sum(t.cache_hit for t in before.values()) + after_hits = sum(t.cache_hit for t in after.values()) + before_misses = sum(t.cache_miss for t in before.values()) + after_misses = sum(t.cache_miss for t in after.values()) + before_total = before_hits + before_misses + after_total = after_hits + after_misses + before_rate = before_hits / before_total if before_total > 0 else 0.0 + after_rate = after_hits / after_total if after_total > 0 else 0.0 + + only_before = set(before) - set(after) + only_after = set(after) - set(before) - except FileNotFoundError: - print(f"Error: File '{file_path}' not found") + print("Cache statistics diff (before → after)") + print() + print( + f"Hits: {before_hits:>12,} → {after_hits:>12,} " + f"({after_hits - before_hits:+,})" + ) + print( + f"Misses: {before_misses:>12,} → {after_misses:>12,} " + f"({after_misses - before_misses:+,})" + ) + print( + f"Hit rate: {before_rate:>11.2%} → {after_rate:>11.2%} " + f"({(after_rate - before_rate) * 100:+.2f} pp)" + ) + print( + f"Tasks: {len(before):>12,} → {len(after):>12,} " + f"(only in before: {len(only_before)}, only in after: {len(only_after)})" + ) + print() + + _print_diff_section( + f"Top {top} INCREASES IN HITS (after > before)", + diffs, + key=lambda d: d.delta_hit, + top=top, + reverse=True, + ) + _print_diff_section( + f"Top {top} INCREASES IN MISSES (after > before)", + diffs, + key=lambda d: d.delta_miss, + top=top, + reverse=True, + ) + _print_diff_section( + f"Top {top} DECREASES IN HITS (after < before)", + diffs, + key=lambda d: d.delta_hit, + top=top, + reverse=False, + ) + _print_diff_section( + f"Top {top} DECREASES IN MISSES (after < before)", + diffs, + key=lambda d: d.delta_miss, + top=top, + reverse=False, + ) + + +def main(): + parser = argparse.ArgumentParser( + description=( + "Analyze turbo_tasks cache effectiveness, or diff two stats files " + "to see which tasks gained or lost hits/misses." + ), + ) + parser.add_argument( + "--diff", + action="store_true", + help="Diff mode: compare two stats files and report largest changes.", + ) + parser.add_argument( + "--top", + type=int, + default=25, + help="Number of entries to show per section in --diff mode (default: 25).", + ) + parser.add_argument( + "files", + nargs="+", + help=( + "Path to stats JSON. In default mode, one file. " + "In --diff mode, two files: ." + ), + ) + + args = parser.parse_args() + + try: + if args.diff: + if len(args.files) != 2: + parser.error("--diff requires exactly two files: ") + before = load_task_stats_map(args.files[0]) + after = load_task_stats_map(args.files[1]) + print_diff(before, after, args.top) + else: + if len(args.files) != 1: + parser.error("default mode requires exactly one stats file") + tasks = load_task_stats(args.files[0]) + tasks = analyze_tasks(tasks) + print_analysis(tasks) + + except FileNotFoundError as e: + print(f"Error: File not found: {e.filename}") sys.exit(1) except json.JSONDecodeError as e: print(f"Error parsing JSON: {e}") sys.exit(1) - except Exception as e: - print(f"Error: {e}") - sys.exit(1) if __name__ == "__main__":