From 8f132ea9848b52e6717959d9ced4efda44ab430d Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 8 May 2026 17:49:29 +0200 Subject: [PATCH 1/7] Fix "type: module" in project dir when using standalone or adapters (#93612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Add `.next/package.json` (the distDir commonjs boundary marker) to the `required-server-files.json` manifest. ### Why? `next build` writes `.next/package.json` with `{"type": "commonjs"}` so that everything in `.next/**` is loaded as CJS even when the user's project `package.json` has `"type": "module"`. Without that file, Node walks up to the project `package.json` when loading `.next/server/**/*.js` and tries to evaluate the compiled server bundles as ESM, which fails at runtime. This file is necessary on disk in every server deployment artifact (the standalone bundle, every adapter Node function), but it was not declared in `required-server-files.json`. As a result, adapters consuming `requiredServerFiles.files` did not include the boundary file. `.next/package.json` is conceptually a required server file: any deployment that wants to run `.next/server/**/*.js` correctly under a `"type": "module"` project must ship it. Declaring it in the manifest is the right place to express that contract once for every consumer. This error case would only happen if the project package.json gets included into the Node.js functions. For example when file tracing analysis (node-file-trace) decides it needs to include it (i.e. glob patterns or such). ### How? Add `'package.json'` to the `requiredServerFilesManifest.files` array in `packages/next/src/build/index.ts`. Every entry in that array is joined with `config.distDir`, so this resolves to `.next/package.json`. Both existing consumers pick it up automatically with no further changes: - `packages/next/src/build/adapter/build-complete.ts` iterates `requiredServerFiles` into `sharedNodeAssets`, which is merged into every Node function's `output.assets` regardless of bundler. The Vercel adapter (and any other adapter) gets the file for free. - `copyTracedFiles` in `packages/next/src/build/utils.ts` iterates `requiredServerFiles.files` and copies each entry into `.next/standalone/`, so the standalone output now contains `.next/standalone/.next/package.json`. ### Tests - `test/production/required-server-files-package-boundary/` — a focused, non-standalone test that builds a `"type": "module"` app and asserts: - `.next/package.json` is `{"type": "commonjs"}` on disk - `.next/required-server-files.json` lists `.next/package.json` - `test/production/standalone-mode/type-module/` — extended with a `pages/dynamic.js` that uses `getServerSideProps`. The test now: - Asserts `.next/standalone/.next/package.json` exists with `{"type": "commonjs"}` - Boots the standalone `server.js` and fetches `/dynamic`, which forces Node to actually evaluate `.next/server/pages/dynamic.js` at runtime — the exact code path the boundary file makes work --- crates/next-api/src/next_server_nft.rs | 3 -- packages/next/src/build/index.ts | 3 ++ .../app/layout.tsx | 11 +++++ .../app/page.tsx | 3 ++ .../my-adapter.mjs | 25 ++++++++++ .../next.config.mjs | 9 ++++ ...ired-server-files-package-boundary.test.ts | 48 +++++++++++++++++++ .../standalone-mode/type-module/index.test.ts | 25 +++++++++- .../type-module/pages/dynamic.js | 7 +++ 9 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 test/production/required-server-files-package-boundary/app/layout.tsx create mode 100644 test/production/required-server-files-package-boundary/app/page.tsx create mode 100644 test/production/required-server-files-package-boundary/my-adapter.mjs create mode 100644 test/production/required-server-files-package-boundary/next.config.mjs create mode 100644 test/production/required-server-files-package-boundary/required-server-files-package-boundary.test.ts create mode 100644 test/production/standalone-mode/type-module/pages/dynamic.js diff --git a/crates/next-api/src/next_server_nft.rs b/crates/next-api/src/next_server_nft.rs index 409b5e01885b..7a22ec686a68 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}"))?; 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/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' } } +} From 4e6331397d2762fd244096991361c4b4a712a45d Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Fri, 8 May 2026 13:10:55 -0700 Subject: [PATCH 2/7] [ci] Also pin first-party GH actions (#93609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were already pinning nearly all of our third-party GitHub actions, but this also pins our first-party ones (things starting with `actions/*`. This gets us closer to being able to enforcing pinning at the repository level: ![Screenshot 2026-05-06 at 4.39.46 PM.png](https://app.graphite.com/user-attachments/assets/ced0b3f2-7de8-4a9a-aa79-8d3efa74197d.png) The one remaining blocker is this self-reference in a `pull_request_target` action: https://github.com/vercel/next.js/blob/c06d94ba22d0156e8bff28c81f7874b73d80ed03/.github/workflows/pull_request_auto_label.yml#L65 I'm still figuring out the best approach to do there, and I'll submit that in a separate PR. Discussion here about enforcing this org-wide: https://vercel.slack.com/archives/C0AM84PRSGL/p1778110550384279 --- .github/actions/setup-rust/action.yml | 2 +- .github/workflows/build_and_deploy.yml | 32 +++++++++---------- .github/workflows/build_and_test.yml | 12 +++---- .github/workflows/build_reusable.yml | 6 ++-- .github/workflows/code_freeze.yml | 2 +- .github/workflows/create_release_branch.yml | 6 ++-- .../workflows/integration_tests_reusable.yml | 2 +- .github/workflows/issue_stale.yml | 8 ++--- .github/workflows/issue_wrong_template.yml | 4 +-- .github/workflows/popular.yml | 4 +-- .github/workflows/pr_ci_comment.yml | 2 +- .github/workflows/pull_request_stats.yml | 6 ++-- .github/workflows/release-next-rspack.yml | 6 ++-- .github/workflows/retry_test.yml | 4 +-- .../rspack-update-tests-manifest.yml | 12 +++---- .../sync_backport_canary_release.yml | 12 +++---- .../test-turbopack-rust-bench-test.yml | 4 +-- .github/workflows/test_e2e_deploy_release.yml | 10 +++--- .../workflows/test_e2e_project_reset_cron.yml | 4 +-- .github/workflows/test_examples.yml | 4 +-- .github/workflows/trigger_release.yml | 6 ++-- .github/workflows/turbopack-benchmark.yml | 6 ++-- .../turbopack-update-tests-manifest.yml | 12 +++---- .github/workflows/update_fonts_data.yml | 6 ++-- .github/workflows/update_react.yml | 6 ++-- .github/workflows/update_react_poller.yml | 2 +- .github/workflows/upload-tests-manifest.yml | 4 +-- .github/workflows/upload_preview_tarballs.yml | 6 ++-- 28 files changed, 95 insertions(+), 95 deletions(-) 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-${{ From e83bc9393cfc79a85a891c59559cbb1d8a815875 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 8 May 2026 13:58:21 -0700 Subject: [PATCH 3/7] Instant Insights: favor reported errors over missing slots (#93709) When we can't validate the entire render because some expected slot wasn't rendered we report that as an error. However we do this before checking if there were errors to report from the slots the did render. Since conditional rendering is a thing and it might inadvertantly cause you to not see more directly actional feedback we now prioritize reported errors from the validation before reporting any unrendered slots --- .../server/app-render/dynamic-rendering.ts | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 10a30c365788..6256a5ede7df 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -1338,30 +1338,14 @@ 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[])] - } - } - + // Missing boundaries on their own aren't a strong signal — a parent + // layout may legitimately omit a slot. Run the remaining validation + // layers first; if they find a genuine problem we'd rather surface + // that. Only fall back to the missing-boundary error when nothing + // else explains the failure (so the user is still made aware that + // validation didn't complete). When we add a markers API, the + // marker-based variant of this check can become strict again. + // // NOTE: We don't care about Suspense above body here, // we're only concerned with the validation boundary if (prelude !== PreludeState.Full) { @@ -1370,13 +1354,14 @@ 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. + 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 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.` @@ -1396,6 +1381,31 @@ export function getNavigationDisallowedDynamicReasons( return [dynamicValidation.dynamicMetadata] } } + + 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[])] + } + } + // We had a non-empty prelude and there are no dynamic holes return [] } From f0c1ffc44ec3182a55345ed67f05533574e8f176 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Fri, 8 May 2026 14:21:52 -0700 Subject: [PATCH 4/7] Remove ineffective turbo-tasks (#91341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Remove ineffective turbo-tasks Identifies and removes turbo-tasks functions where the task overhead exceeds the value they provide. Each turbo-task carries ~4-6μs execution overhead per miss and ~200-500ns per cache hit, plus allocations and bookkeeping. ### What? Removes 22 `#[turbo_tasks::function]` implementations across resolve plugins, chunk items, and resolve-result helpers — converting them to plain methods or inlining their work. Changes fall into a few buckets: - **ResolvePlugin condition handling** (`AfterResolvePluginCondition::matches`, `BeforeResolvePluginCondition::matches`, `after_resolve_condition`, `before_resolve_condition`): conditions now store the resolved `Glob` as a `ReadRef` on the plugin struct at construction, so `matches` is a pure sync function and the per-plugin `*_resolve_condition` getters are trivial field reads (no longer turbo-tasks). The `after_resolve` / `before_resolve` hooks themselves stay as `#[turbo_tasks::function]` — they synthesize virtual sources/modules and need memoization on `(self, lookup_path, reference_type, request)` to avoid distinct cells producing duplicate module-graph idents. - The basic theory here is that the right level of caching is at `resolve` and at the hook bodies themselves, not the conditions or condition getters. - `AfterResolvePluginCondition` and `BeforeResolvePluginCondition` are marked `serialization = "none"` because `ReadRef` cannot be persisted; plugin construction is cheap enough to re-derive on restore. - **ChunkItem trait methods** (`chunking_context`, `ty`, `content_with_async_module_info`): returned constants or simple field reads, zero cache hits and no `.await` calls (no invalidation value). - **ResolveResult / ModuleResolveResult helpers** (`primary_modules`, `first_module`, `first_source`, `primary_sources`, `is_unresolvable`, `primary_output_assets`): simple iterators over already-resolved data; converted to plain methods. Added a `Duplicate(usize)` variant to `ModuleResolveResultItem` to handle dedup at construction time instead of in a separate task. - The basic idea here is that it is reasonable to consume `ResolveResult/ModuleResolveResult` monolithically, and we get little to no benefit from fine grained access. e.g. `is_unresolved()` in theory that is a valuable turbotask, but since it rarely changes but generally if we change how we resolve an import then we have to regenerate code, so saving a few boolean conditions is unlikely to be very valuable. - Misc: `EcmascriptModuleAsset::analyze`, `is_types_resolving_enabled`, `next_server::resolve::condition`. ### Impact (vercel-site build, dev first-compile) | Metric | Before | After | Δ | |---|---:|---:|---:| | Total cache hits | 30,885,827 | 29,201,314 | −1,684,513 | | Total cache misses | 6,473,123 | 5,953,626 | **−519,497** | | Overall hit rate | 82.67% | 83.06% | +0.39 pp | | Registered task functions | 1,294 | 1,272 | −22 | The 22 removed tasks were collectively responsible for ~519K misses per build — each miss previously paying the full execution overhead. Most of the work from `EcmascriptModuleAsset::analyze` naturally migrated into `analyze_ecmascript_module` (the task it was wrapping; +129K hits there). ### On-disk cache size (persistent caching) Each removed task also stops allocating cache cells on disk. Measured on the same vercel-site build with `.next/cache/turbopack` (persistent cache enabled): | | Size | |---|---:| | canary | 2.56 GiB | | this branch | 2.46 GiB | | **saved** | **~100 MiB (−3.81%)** | ### Build-time wall clock and peak memory Ran `pnpm next build --experimental-build-mode=compile` 5 times on each branch **Peak RSS — clear reduction:** | | canary | branch | Δ | |---|---:|---:|---:| | min | 19.18 GiB | 18.94 GiB | | | **median** | **19.22 GiB** | **19.01 GiB** | **−217 MiB (−1.10%)** | | mean | 19.21 GiB | 19.02 GiB | −199 MiB (−1.01%) | | max | 19.23 GiB | 19.13 GiB | | Every branch run has lower RSS than every canary run — the distributions don't overlap. Welch's t = −6.03. **Wall time — no measurable change:** | | canary | branch | Δ | |---|---:|---:|---:| | min | 62.03s | 60.78s | | | **median** | **62.61s** | **62.65s** | **+0.04s (+0.06%)** | | mean | 62.83s | 63.80s | +0.96s (+1.53%) | | max | 64.25s | 68.23s | | | stddev | 0.84s | 3.42s | | Median is flat. The mean difference is within noise (Welch's t = +0.61, n = 5). Branch run-to-run variance is higher — one 68.23s outlier pulls the mean up — so this is neither a regression nor a measurable speedup at this sample size. --- crates/next-api/src/app.rs | 1 - crates/next-api/src/next_server_nft.rs | 3 +- crates/next-api/src/pages.rs | 1 + .../src/next_client/runtime_entry.rs | 1 - .../ecmascript_client_reference_module.rs | 17 +- crates/next-core/src/next_font/local/mod.rs | 17 +- crates/next-core/src/next_import_map.rs | 2 +- crates/next-core/src/next_server/resolve.rs | 40 +- crates/next-core/src/next_shared/resolve.rs | 114 ++-- .../transforms/swc_ecma_transform_plugins.rs | 6 +- .../src/next_shared/webpack_rules/babel.rs | 4 +- .../crates/turbo-tasks-fs/src/read_glob.rs | 2 +- .../turbopack-browser/src/react_refresh.rs | 4 +- .../turbopack-cli-utils/src/runtime_entry.rs | 1 - .../crates/turbopack-cli/src/build/mod.rs | 1 + .../turbopack-cli/src/dev/web_entry_source.rs | 9 +- .../turbopack-core/src/chunk/chunk_group.rs | 25 +- .../src/chunk/chunk_item_batch.rs | 28 +- .../turbopack-core/src/chunk/chunking/dev.rs | 2 +- .../turbopack-core/src/chunk/chunking/mod.rs | 17 +- .../src/chunk/chunking/style_production.rs | 2 +- .../src/chunk/chunking_context.rs | 6 +- .../crates/turbopack-core/src/chunk/mod.rs | 13 +- .../turbopack-core/src/introspect/utils.rs | 1 - .../src/module_graph/style_groups.rs | 10 +- .../turbopack-core/src/reference/mod.rs | 12 +- .../turbopack-core/src/resolve/error.rs | 4 +- .../crates/turbopack-core/src/resolve/mod.rs | 488 ++++++++++++++---- .../turbopack-core/src/resolve/plugin.rs | 70 ++- turbopack/crates/turbopack-css/src/asset.rs | 4 - .../crates/turbopack-css/src/module_asset.rs | 7 +- .../turbopack-css/src/references/compose.rs | 6 +- .../turbopack-css/src/references/url.rs | 2 +- .../src/chunk/chunk_type.rs | 18 +- .../turbopack-ecmascript/src/chunk/item.rs | 56 +- .../crates/turbopack-ecmascript/src/lib.rs | 9 +- .../src/references/esm/base.rs | 6 +- .../src/references/mod.rs | 5 +- .../src/references/pattern_mapping.rs | 46 +- .../turbopack-node/src/transforms/webpack.rs | 4 +- .../src/node_native_binding.rs | 12 +- .../turbopack-resolve/src/typescript.rs | 31 +- turbopack/crates/turbopack/src/lib.rs | 20 +- .../scripts/analyze_cache_effectiveness.py | 229 +++++++- 44 files changed, 931 insertions(+), 425 deletions(-) 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 7a22ec686a68..5335de042fe5 100644 --- a/crates/next-api/src/next_server_nft.rs +++ b/crates/next-api/src/next_server_nft.rs @@ -242,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/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__": From b4804249cf4775d159452d0226be8038d507d43e Mon Sep 17 00:00:00 2001 From: "next-js-bot[bot]" <279046576+next-js-bot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 23:36:03 +0000 Subject: [PATCH 5/7] v16.3.0-canary.17 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-internal/package.json | 2 +- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-playwright/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-routing/package.json | 2 +- packages/next-rspack/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 21 files changed, 36 insertions(+), 36 deletions(-) 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/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 From ac777e2c85652684bb7e43e910765e78f3051410 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 8 May 2026 19:56:04 -0400 Subject: [PATCH 6/7] Remove redundant instant navigation prefetch header (#93713) `NEXT_INSTANT_PREFETCH_HEADER` was replaced by `NEXT_INSTANT_TEST_COOKIE` in #89871. Deleting the dead code. --- packages/next/src/build/templates/app-page.ts | 22 ++++++++++-------- .../client/components/app-router-headers.ts | 14 +++-------- .../router-reducer/fetch-server-response.ts | 2 -- .../client/components/segment-cache/cache.ts | 23 ------------------- .../stream-utils/node-web-streams-helper.ts | 8 +++---- 5 files changed, 19 insertions(+), 50 deletions(-) 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/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({ From 938c286bac984aa7275bb4c18aa0c154b443aa93 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 8 May 2026 17:13:29 -0700 Subject: [PATCH 7/7] Instant Insights: Favor reporting errors from all potential navigations over reporting a failed attempt to validate when a slot is missing (#93714) When instant UI validation cannot successfully complete at a given simluated navigation level it previously would halt the validation there and report the error. This is fine however it is possible that the cause of the impcomplete validation is mundane (i.e. conditionally rendering a slot) and focussing on this lack of ability fully validate a level when other simulated navigations might also provide insights means we might send you down the wrong line of investigation when something more actionable already exists. In the long run these insights ought to be provided through a non-error UI. in that world we'd probably want to perform all the validations to their extent and then simply notify you about which ones could and could not be completed. However since we currently use the error overlay to convey these issues we will favor instead showing actionable errors from any validation depth before showing you less actionable "could not validate" errors --- .../next/src/server/app-render/app-render.tsx | 88 +++++++++++-------- .../server/app-render/dynamic-rendering.ts | 54 ++++++++---- .../app/suspense-in-root/page.tsx | 3 + .../inner/layout.tsx | 7 ++ .../inner/page.tsx | 11 +++ .../multi-depth-deferred-fallback/layout.tsx | 14 +++ .../multi-depth-deferred-fallback/page.tsx | 5 ++ .../instant-validation-parallel-slots.test.ts | 39 ++++++-- .../instant-validation.test.ts | 51 +++++++++++ 9 files changed, 211 insertions(+), 61 deletions(-) create mode 100644 test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/layout.tsx create mode 100644 test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/inner/page.tsx create mode 100644 test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/layout.tsx create mode 100644 test/e2e/app-dir/instant-validation/app/suspense-in-root/static/multi-depth-deferred-fallback/page.tsx 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 6256a5ede7df..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,14 +1352,6 @@ export function getNavigationDisallowedDynamicReasons( return validationPreventingErrors } - // Missing boundaries on their own aren't a strong signal — a parent - // layout may legitimately omit a slot. Run the remaining validation - // layers first; if they find a genuine problem we'd rather surface - // that. Only fall back to the missing-boundary error when nothing - // else explains the failure (so the user is still made aware that - // validation didn't complete). When we add a markers API, the - // marker-based variant of this check can become strict again. - // // NOTE: We don't care about Suspense above body here, // we're only concerned with the validation boundary if (prelude !== PreludeState.Full) { @@ -1360,13 +1366,11 @@ export function getNavigationDisallowedDynamicReasons( allRequiredBoundariesRendered(boundaryState) ) { // If we ever get this far then we messed up the tracking of invalid - // dynamic. (When boundaries are missing the 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.` - ), - ] + // 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 @@ -1382,6 +1386,12 @@ export function getNavigationDisallowedDynamicReasons( } } + // 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] @@ -1390,19 +1400,25 @@ export function getNavigationDisallowedDynamicReasons( const error = rootInstantStack !== null ? rootInstantStack() : new Error() error.name = 'Error' error.message = message - return [error] + 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] + 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 [error, ...(thrownErrorsOutsideBoundary as Error[])] + return new AggregateError([ + error, + ...(thrownErrorsOutsideBoundary as Error[]), + ]) } } 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) {