From d6e57416822e2d640da62213e0622708eea19292 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:43:29 +0200 Subject: [PATCH 1/9] ci: remove pull_request_stats workflow (#97792) This was quite useless (very flaky) and furthermore also slow --- .github/workflows/pr_ci_comment.yml | 2 +- .github/workflows/pull_request_stats.yml | 204 ----------------------- scripts/pr-ci-comment.mjs | 130 --------------- 3 files changed, 1 insertion(+), 335 deletions(-) delete mode 100644 .github/workflows/pull_request_stats.yml diff --git a/.github/workflows/pr_ci_comment.yml b/.github/workflows/pr_ci_comment.yml index 6863bdf0098f..2df053760c21 100644 --- a/.github/workflows/pr_ci_comment.yml +++ b/.github/workflows/pr_ci_comment.yml @@ -2,7 +2,7 @@ name: PR CI Comment on: workflow_run: - workflows: ['build-and-test', 'Generate Stats'] + workflows: ['build-and-test'] types: [requested, completed] permissions: diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml deleted file mode 100644 index 92e85e87a89a..000000000000 --- a/.github/workflows/pull_request_stats.yml +++ /dev/null @@ -1,204 +0,0 @@ -on: - pull_request: - types: [opened, synchronize] - push: - branches: - - canary - -name: Generate Stats - -concurrency: - # Keep cancel-on-update behavior for PRs, but allow canary pushes to run independently. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - NAPI_CLI_VERSION: 3.7.2 - TURBO_VERSION: 2.9.4 - TEST_CONCURRENCY: 6 - - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - # Prefer shared remote cache across runs, but keep local cache enabled so jobs - # degrade gracefully if the remote cache or token is unavailable. - TURBO_CACHE: 'local:rw,remote:rw' - NEXT_TELEMETRY_DISABLED: 1 - # Vercel KV Store for test timings - KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }} - KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }} - NEXT_TEST_JOB: 1 - NEXT_DISABLE_SWC_WASM: 1 - -jobs: - pr-ci-metadata: - name: Upload PR CI metadata - if: ${{ github.event_name == 'pull_request' }} - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Write PR metadata - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} - PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} - PR_BASE_REF: ${{ github.event.pull_request.base.ref }} - PR_IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - run: | - mkdir -p pr-ci-metadata - node <<'NODE' - const fs = require('fs') - - fs.writeFileSync( - 'pr-ci-metadata/pr.json', - JSON.stringify( - { - number: Number(process.env.PR_NUMBER), - headSha: process.env.PR_HEAD_SHA, - headRef: process.env.PR_HEAD_REF, - headRepo: process.env.PR_HEAD_REPO, - baseRef: process.env.PR_BASE_REF, - isFork: process.env.PR_IS_FORK === 'true', - }, - null, - 2 - ) - ) - NODE - - - name: Upload PR metadata - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: pr-ci-metadata - path: pr-ci-metadata/pr.json - retention-days: 1 - - build: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/build_reusable.yml - secrets: inherit - with: - stepName: 'generate-pull-request-stats' - uploadSwcArtifact: 'yes' - uploadAnalyzerArtifacts: 'yes' - - stats: - name: Stats (${{ matrix.bundler }}) - needs: build - # The webpack benchmark alone takes ~25 minutes, so anything at or below - # that cancels the job right as it finishes (seen on canary and PRs alike). - timeout-minutes: 35 - strategy: - fail-fast: false - matrix: - # Keep in sync with STATS_EXPECTED_BUNDLERS - bundler: [webpack, turbopack] - runs-on: ubuntu-latest-16-core-arm-oss - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 25 - persist-credentials: false - - - name: Check non-docs only change - run: echo "DOCS_CHANGE<> $GITHUB_OUTPUT; echo "$(node scripts/run-for-change.mjs --not --type docs --exec echo 'nope')" >> $GITHUB_OUTPUT; echo 'EOF' >> $GITHUB_OUTPUT - id: docs-change - - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - with: - name: next-swc-binary-${{ runner.os }}-${{ runner.arch }} - path: packages/next-swc/native - - - run: cp -r packages/next-swc/native .github/actions/next-stats-action/native - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - - - name: Set up Turborepo remote cache - if: ${{ vars.TURBO_TEAM != '' }} - continue-on-error: true - uses: vercel/setup-turborepo-remote-cache-action@135046f5efcadb17337d9b29b8d5b4035ae64b10 # v1.0.0 - with: - team: ${{ vars.TURBO_TEAM }} - - # `TURBO_TOKEN` reaches this Docker action through the job environment. - - uses: ./.github/actions/next-stats-action - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - with: - bundler: ${{ matrix.bundler }} - env: - TURBO_TEAM: ${{ env.TURBO_TEAM }} - TURBO_CACHE: ${{ env.TURBO_CACHE }} - PREVIEW_BUILDS_BASE_URL: ${{ vars.PREVIEW_BUILDS_BASE_URL }} - - - name: Upload stats results - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: pr-stats-${{ matrix.bundler }} - path: pr-stats-${{ matrix.bundler }}.json - retention-days: 1 - - stats-aggregate: - name: Aggregate Stats - needs: stats - # Always run so we can recover partial results. A single bundler timing out - # cancels its job, which makes `cancelled()` true here and marks the whole - # run "cancelled" even though the other bundler finished, so `!cancelled()` - # would wrongly skip aggregation. Running unconditionally lets us aggregate - # whatever bundlers succeeded; the reason a bundler is missing is reported in - # the comment, and a truly cancelled run (no results at all) is handled by - # the cancelled fallback in scripts/pr-ci-comment.mjs. - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 25 - persist-credentials: false - - - name: Check non-docs only change - run: echo "DOCS_CHANGE<> $GITHUB_OUTPUT; echo "$(node scripts/run-for-change.mjs --not --type docs --exec echo 'nope')" >> $GITHUB_OUTPUT; echo 'EOF' >> $GITHUB_OUTPUT - id: docs-change - - - name: Download all stats artifacts - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: pr-stats-* - path: stats-results - merge-multiple: true - - - name: Setup Node.js - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: .node-version - package-manager-cache: false - - - name: Install dependencies - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - working-directory: .github/actions/next-stats-action - run: npm install - - - name: Aggregate and post results - if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} - working-directory: .github/actions/next-stats-action - run: node src/aggregate-results.js ${{ github.workspace }}/stats-results - env: - KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }} - KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }} - # Keep in sync with the `stats` job matrix above. The aggregate uses it - # to detect and report bundlers whose job failed or was cancelled - # (their pr-stats-.json artifact will be missing). - STATS_EXPECTED_BUNDLERS: 'webpack turbopack' - # Collapsed matrix result ('success' | 'failure' | 'cancelled' | - # 'skipped'). Used to decide, when no artifacts exist at all, whether - # the bundlers genuinely failed (post an "all failed" notice) or the - # run was cancelled/superseded (stay silent and let pr-ci-comment.mjs - # post the cancelled notice). - STATS_RESULT: ${{ needs.stats.result }} diff --git a/scripts/pr-ci-comment.mjs b/scripts/pr-ci-comment.mjs index fdc8ff2a22a8..3e137cec99c5 100644 --- a/scripts/pr-ci-comment.mjs +++ b/scripts/pr-ci-comment.mjs @@ -6,7 +6,6 @@ import path from 'node:path' import { buildTestReport } from './test-report.js' const TEST_COMMENT_MARKER = '' -const STATS_COMMENT_MARKER = '' const MAX_COMMENT_LENGTH = 62_000 const MAX_RESULT_MESSAGE_LENGTH = 12_000 @@ -296,11 +295,6 @@ async function main() { return } - if (workflowRun.name === 'Generate Stats') { - await handleStatsWorkflow({ github, workflowRun, pr, phase }) - return - } - if (workflowRun.name === 'build-and-test') { await handleBuildAndTestWorkflow({ github, @@ -457,115 +451,6 @@ async function readPullRequestMetadataArtifact() { } } -async function handleStatsWorkflow({ github, workflowRun, pr, phase }) { - if (phase === 'requested') { - const sha = pr.headSha || workflowRun.head_sha - const body = [ - STATS_COMMENT_MARKER, - '## Stats in progress', - '', - `Commit: ${sha}`, - `[View workflow run](${workflowRun.html_url})`, - '', - ].join('\n') - - await github.insertIssueCommentIfMissing( - pr.number, - STATS_COMMENT_MARKER, - body, - ['## Stats from current PR'] - ) - return - } - - // Look for a stats block before reacting to the run conclusion. A single - // bundler timing out cancels its job and marks the whole run "cancelled", but - // the aggregate still emits stats for the bundlers that finished. Treating a - // cancelled run as "no data" up front would discard that partial comment, so - // we post whatever the aggregate produced first and only fall back to a - // cancelled/skipped notice when there is genuinely no block. - const jobs = await github.listJobsForRunAttempt( - workflowRun.id, - workflowRun.run_attempt || 1 - ) - const candidates = jobs.filter((job) => /aggregate stats/i.test(job.name)) - - for (const job of candidates) { - let logs - try { - logs = await github.downloadJobLogs(job.id) - } catch (err) { - // A cancelled or skipped aggregate job may have no retrievable logs. - console.log(`Failed to download logs for job ${job.id}`, err) - continue - } - - const stats = extractDelimitedBlock( - logs, - '--stats start--', - '--stats end--' - ) - - if (!stats) { - continue - } - - let body = stats - .replace('âš ï¸', '\u26a0\ufe0f') - .replace('✓', '\u2713') - .trim() - - if (!body.includes(STATS_COMMENT_MARKER)) { - body = `${STATS_COMMENT_MARKER}\n${body}` - } - - body += `\n\nCommit: ${pr.headSha || workflowRun.head_sha}` - - await github.upsertIssueComment(pr.number, STATS_COMMENT_MARKER, body, [ - '## Stats from current PR', - ]) - return - } - - const sha = pr.headSha || workflowRun.head_sha - - // No stats block. A cancelled conclusion here means the whole run was - // cancelled (superseded, or every bundler cancelled) rather than an - // individual bundler, since a partial run would have produced a block above. - if (workflowRun.conclusion === 'cancelled') { - console.log('No stats block found and the run was cancelled.') - const body = [ - STATS_COMMENT_MARKER, - '## Stats cancelled', - '', - `Commit: ${sha}`, - `[View workflow run](${workflowRun.html_url})`, - '', - ].join('\n') - - await github.upsertIssueComment(pr.number, STATS_COMMENT_MARKER, body, [ - '## Stats from current PR', - ]) - return - } - - console.log( - 'No stats block found in the completed stats workflow. Assuming stats were skipped.' - ) - - const body = [ - STATS_COMMENT_MARKER, - '## Stats skipped', - '', - `Commit: ${sha}`, - `[View workflow run](${workflowRun.html_url})`, - '', - ].join('\n') - await github.upsertIssueComment(pr.number, STATS_COMMENT_MARKER, body, [ - '## Stats from current PR', - ]) -} - async function handleBuildAndTestWorkflow({ github, workflowRun, @@ -716,21 +601,6 @@ function extractJsonBlocks(logs) { return blocks } -function extractDelimitedBlock(logs, start, end) { - const startIndex = logs.indexOf(start) - if (startIndex === -1) { - return null - } - - const contentStart = startIndex + start.length - const endIndex = logs.indexOf(end, contentStart) - if (endIndex === -1) { - return null - } - - return logs.slice(contentStart, endIndex).trim() -} - function buildTestReportComment({ failedSuites, otherFailures, From d75bf1b85ac5f8815188d42588cca3c0d90a5d2c Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:49:52 +0200 Subject: [PATCH 2/9] Fix build error when aliasing `typescript` to `@typescript/typescript6` (#97942) This was previously broken, because the binary isn't called `tsc` but `tsc6`: ```json "dependencies": { "typescript": "npm:@typescript/typescript6@6.0.1" } ``` --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> --- .../lib/typescript/runTypeScriptCli.test.ts | 110 +++++++++++++++++- .../src/lib/typescript/runTypeScriptCli.ts | 7 +- .../next/src/lib/verify-typescript-setup.ts | 4 +- .../typescript-cli/typescript-cli.test.ts | 23 ++++ 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/packages/next/src/lib/typescript/runTypeScriptCli.test.ts b/packages/next/src/lib/typescript/runTypeScriptCli.test.ts index f3a18edd0bfd..ed6bdb662377 100644 --- a/packages/next/src/lib/typescript/runTypeScriptCli.test.ts +++ b/packages/next/src/lib/typescript/runTypeScriptCli.test.ts @@ -1,5 +1,8 @@ import type { ChildProcess } from 'node:child_process' import { EventEmitter } from 'node:events' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' import { PassThrough } from 'node:stream' const mockSpawn = jest.fn() @@ -9,9 +12,114 @@ jest.mock('next/dist/compiled/cross-spawn', () => ({ default: (...args: unknown[]) => mockSpawn(...args), })) -const { runTypeScriptCli } = +const { getTypeScriptPackageInfo, runTypeScriptCli } = require('./runTypeScriptCli') as typeof import('./runTypeScriptCli') +function createTypeScriptFixture({ + packageJson, + files, +}: { + packageJson: Record + files: string[] +}) { + const baseDir = mkdtempSync(path.join(tmpdir(), 'next-typescript-')) + const packageDir = path.join(baseDir, 'node_modules', 'typescript') + + mkdirSync(packageDir, { recursive: true }) + writeFileSync( + path.join(packageDir, 'package.json'), + JSON.stringify(packageJson) + ) + + for (const file of files) { + const filePath = path.join(packageDir, file) + mkdirSync(path.dirname(filePath), { recursive: true }) + writeFileSync(filePath, '') + } + + return { baseDir, packageDir } +} + +function removeTypeScriptFixture(baseDir: string) { + rmSync(baseDir, { recursive: true, force: true }) +} + +describe('getTypeScriptPackageInfo', () => { + it('resolves a standard tsc bin', () => { + const { baseDir, packageDir } = createTypeScriptFixture({ + packageJson: { + version: '6.0.1', + bin: { tsc: './bin/tsc' }, + }, + files: ['bin/tsc', 'lib/typescript.js'], + }) + + try { + expect(getTypeScriptPackageInfo(baseDir)).toMatchObject({ + apiPath: path.join(packageDir, 'lib', 'typescript.js'), + tscPath: path.join(packageDir, 'bin', 'tsc'), + }) + } finally { + removeTypeScriptFixture(baseDir) + } + }) + + it('resolves a versioned tsc bin from an aliased TypeScript package', () => { + const { baseDir, packageDir } = createTypeScriptFixture({ + packageJson: { + name: '@typescript/typescript6', + version: '6.0.1', + bin: { tsc6: './bin/tsc6' }, + }, + files: ['bin/tsc6', 'lib/typescript.js'], + }) + + try { + expect(getTypeScriptPackageInfo(baseDir)).toMatchObject({ + apiPath: path.join(packageDir, 'lib', 'typescript.js'), + tscPath: path.join(packageDir, 'bin', 'tsc6'), + }) + } finally { + removeTypeScriptFixture(baseDir) + } + }) + + it('does not return a tsc path when the declared bin is missing', () => { + const { baseDir } = createTypeScriptFixture({ + packageJson: { + version: '6.0.1', + bin: { tsc6: './bin/tsc6' }, + }, + files: ['lib/typescript.js'], + }) + + try { + expect(getTypeScriptPackageInfo(baseDir)?.tscPath).toBeUndefined() + } finally { + removeTypeScriptFixture(baseDir) + } + }) + + it('uses the JS CLI wrapper for an extensionless ESM bin', () => { + const { baseDir, packageDir } = createTypeScriptFixture({ + packageJson: { + version: '7.0.0', + type: 'module', + bin: { tsc: './bin/tsc' }, + }, + files: ['bin/tsc', 'lib/tsc.js'], + }) + + try { + expect(getTypeScriptPackageInfo(baseDir)?.tscPath).toBe( + path.join(packageDir, 'lib', 'tsc.js') + ) + } finally { + removeTypeScriptFixture(baseDir) + } + }) +}) + const processEvents = ['exit', 'SIGINT', 'SIGTERM', 'SIGHUP'] as const type ProcessEvent = (typeof processEvents)[number] diff --git a/packages/next/src/lib/typescript/runTypeScriptCli.ts b/packages/next/src/lib/typescript/runTypeScriptCli.ts index a4c7cdde56cd..e461b7cc0df6 100644 --- a/packages/next/src/lib/typescript/runTypeScriptCli.ts +++ b/packages/next/src/lib/typescript/runTypeScriptCli.ts @@ -33,7 +33,12 @@ export function getTypeScriptPackageInfo( const packageDir = path.dirname(packageJsonPath) const apiPath = path.join(packageDir, 'lib', 'typescript.js') const tscBin = - typeof packageJson.bin === 'string' ? packageJson.bin : packageJson.bin?.tsc + typeof packageJson.bin === 'string' + ? packageJson.bin + : (packageJson.bin?.tsc ?? + Object.entries(packageJson.bin ?? {}).find(([name]) => + /^tsc\d+$/.test(name) + )?.[1]) const tscBinPath = tscBin ? path.resolve(packageDir, tscBin) : undefined let tscPath = tscBinPath diff --git a/packages/next/src/lib/verify-typescript-setup.ts b/packages/next/src/lib/verify-typescript-setup.ts index 6a7bb600033c..31e0ab67575a 100644 --- a/packages/next/src/lib/verify-typescript-setup.ts +++ b/packages/next/src/lib/verify-typescript-setup.ts @@ -31,7 +31,9 @@ const typescriptApiPackage: MissingDependency = { } const typescriptCliPackage: MissingDependency = { - file: 'typescript/bin/tsc', + // The CLI path is resolved from the package's bin metadata after this initial + // package-presence check, since aliased TypeScript packages may rename it. + file: 'typescript/package.json', pkg: 'typescript', exportsRestrict: true, } diff --git a/test/production/app-dir/typescript-cli/typescript-cli.test.ts b/test/production/app-dir/typescript-cli/typescript-cli.test.ts index 523d9573ea33..0202e12c3234 100644 --- a/test/production/app-dir/typescript-cli/typescript-cli.test.ts +++ b/test/production/app-dir/typescript-cli/typescript-cli.test.ts @@ -169,4 +169,27 @@ describe('TypeScript CLI backend', () => { expect(await next.hasFile('.next/cache/.tsbuildinfo')).toBe(true) }) }) + + describe('TypeScript 6 npm alias', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + dependencies: { + typescript: 'npm:@typescript/typescript6@6.0.1', + }, + }) + + if (skipped) return + + it('builds with the versioned tsc6 entry point', async () => { + const result = await next.build() + + expect(result.exitCode).toBe(0) + expect(result.cliOutput).not.toContain( + 'do not have the required package(s) installed' + ) + expect(await next.hasFile('.next/cache/.tsbuildinfo')).toBe(true) + }) + }) }) From 80f846c74e1caccfb3c096c99ec4a395de5f910c Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:55:24 +0200 Subject: [PATCH 3/9] Support immutable static assets with `output: 'export'` (#97711) There was no real reason why this wasn't done thus far. `output: 'export'` already uses the adapter anyway. Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> --- .../next/src/build/adapter/build-complete.ts | 24 +++++----- packages/next/src/server/config.ts | 12 ++--- .../export-immutable-assets.test.ts | 44 ++++++++++++++----- .../export-immutable-assets/my-adapter.mjs | 19 ++++++++ .../export-immutable-assets/next.config.js | 4 +- 5 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 test/production/export-immutable-assets/my-adapter.mjs diff --git a/packages/next/src/build/adapter/build-complete.ts b/packages/next/src/build/adapter/build-complete.ts index 2652682e0b37..38925acdd3d6 100644 --- a/packages/next/src/build/adapter/build-complete.ts +++ b/packages/next/src/build/adapter/build-complete.ts @@ -17,6 +17,7 @@ import { } from '../../shared/lib/router/utils/app-paths' import { AdapterOutputType, type PHASE_TYPE } from '../../shared/lib/constants' import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' +import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' import { convertRedirects, convertRewrites, @@ -661,6 +662,16 @@ export async function handleBuildComplete({ staticFiles: [], } + const clientHashes: Record | undefined = + bundler === Bundler.Turbopack && config.supportsImmutableAssets + ? JSON.parse( + await fs.readFile( + path.join(distDir, 'immutable-static-hashes.json'), + 'utf8' + ) + ) + : undefined + if (config.output === 'export') { // collect export assets and provide as static files const exportFiles = await recursiveReadDir(configOutDir) @@ -672,27 +683,18 @@ export async function handleBuildComplete({ pathname = pathname.startsWith('/') ? pathname : `/${pathname}` + const immutableId = normalizePathSep(file).replace(/^\/?_next\//, '') outputs.staticFiles.push({ id: file, pathname, filePath: path.join(configOutDir, file), type: AdapterOutputType.STATIC_FILE, - immutableHash: undefined, + immutableHash: clientHashes?.[immutableId], } satisfies AdapterOutput['STATIC_FILE']) } } else { const staticFiles = await recursiveReadDir(path.join(distDir, 'static')) - const clientHashes: Record | undefined = - bundler === Bundler.Turbopack && config.supportsImmutableAssets - ? JSON.parse( - await fs.readFile( - path.join(distDir, 'immutable-static-hashes.json'), - 'utf8' - ) - ) - : undefined - for (const file of staticFiles) { const pathname = path.posix.join('/_next/static', file) const filePath = path.join(distDir, 'static', file) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 7db6921588bd..6858f8d3056c 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1699,14 +1699,10 @@ function finalizeConfig( config.supportsImmutableAssets = false } - if ( - config.supportsImmutableAssets && - (config.output === 'export' || config.output === 'standalone') - ) { - // supportsImmutableAssets is designed to work with adapters. Disable it for output=export and - // output=standalone, which are currently using a non-adapter codepath. - // Particularly output=export should just run through the adapter, with only static assets. - // TODO remove again once output=export (and output=standalone) are using adapters. + if (config.supportsImmutableAssets && config.output === 'standalone') { + // supportsImmutableAssets is designed to work with adapters. Disable it for output=standalone, + // which is currently using a non-adapter codepath. + // TODO remove again once output=standalone is using adapters. config.supportsImmutableAssets = false } diff --git a/test/production/export-immutable-assets/export-immutable-assets.test.ts b/test/production/export-immutable-assets/export-immutable-assets.test.ts index 3e2d28b100be..3433771213df 100644 --- a/test/production/export-immutable-assets/export-immutable-assets.test.ts +++ b/test/production/export-immutable-assets/export-immutable-assets.test.ts @@ -1,10 +1,11 @@ import { nextTestSetup } from 'e2e-utils' import { join } from 'path' +import type { NextAdapter } from 'next' import { listClientChunks } from 'next-test-utils' -// Immutable static files are only supported with Turbopack anywy +// Immutable static files are only supported with Turbopack anyway ;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( - 'output: export - immutable assets disabled', + 'output: export - immutable assets', () => { const { next } = nextTestSetup({ files: __dirname, @@ -13,19 +14,40 @@ import { listClientChunks } from 'next-test-utils' disableAutoSkewProtection: true, }) - // supportsImmutableAssets is designed to work with adapters. It must be - // force-disabled for `output: 'export'`, so even though it is requested here - // the build should not emit any `_next/static/immutable` assets. - // We can loosen this requirement in the future when using output=export together with an adapter - it('does not emit any _next/static/immutable files', async () => { + it('emits immutable assets and reports their hashes', async () => { const { exitCode } = await next.build() expect(exitCode).toBe(0) - const files = await listClientChunks(join(next.testDir, next.distDir)) + const files = await listClientChunks(join(next.testDir, 'out', '_next')) + expect(files).toContainEqual(expect.stringContaining('static/immutable/')) + expect(files).not.toContainEqual( + expect.stringMatching(/^static\/chunks\//) + ) - expect(files).not.toBeEmpty() - expect(files).toSatisfyAll((f) => f.includes('static/')) - expect(files).toSatisfyAll((f) => !f.includes('static/immutable/')) + const html = await next.readFile('out/index.html') + expect(html).toContain('/_next/static/immutable/') + expect(html).not.toContain('?dpl=test-deployment-id') + + const hashes = await next.readJSON( + join(next.distDir, 'immutable-static-hashes.json') + ) + const staticFiles: Parameters< + NextAdapter['onBuildComplete'] + >[0]['outputs']['staticFiles'] = await next.readJSON( + 'build-complete.json' + ) + + let immutableFileCount = 0 + for (const output of staticFiles) { + const id = output.id + .replace(/^[/\\]?_next[/\\]/, '') + .replaceAll('\\', '/') + expect(output.immutableHash).toBe(hashes[id]) + if (output.immutableHash) { + immutableFileCount++ + } + } + expect(immutableFileCount).toBeGreaterThan(0) }) } ) diff --git a/test/production/export-immutable-assets/my-adapter.mjs b/test/production/export-immutable-assets/my-adapter.mjs new file mode 100644 index 000000000000..d197982a0296 --- /dev/null +++ b/test/production/export-immutable-assets/my-adapter.mjs @@ -0,0 +1,19 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +/** @type {import('next').NextAdapter} */ +const adapter = { + name: 'export-immutable-assets-test-adapter', + modifyConfig(config) { + config.supportsImmutableAssets = true + return config + }, + async onBuildComplete({ projectDir, outputs }) { + await fs.writeFile( + path.join(projectDir, 'build-complete.json'), + JSON.stringify(outputs.staticFiles) + ) + }, +} + +export default adapter diff --git a/test/production/export-immutable-assets/next.config.js b/test/production/export-immutable-assets/next.config.js index d2f0f7917cb5..2b66a7978b54 100644 --- a/test/production/export-immutable-assets/next.config.js +++ b/test/production/export-immutable-assets/next.config.js @@ -4,9 +4,7 @@ const nextConfig = { output: 'export', deploymentId: 'test-deployment-id', - experimental: { - supportsImmutableAssets: true, - }, + adapterPath: require.resolve('./my-adapter.mjs'), } module.exports = nextConfig From 4160afe2c6bc4ecd09b10a283963ba2305d7c54b Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:58:44 +0200 Subject: [PATCH 4/9] Turbopack: allow compiling turbopack-node without a pool backend (#97943) It failed to compile thus far if you didn't set any features. Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> --- turbopack/crates/turbopack-node/src/pool_stats.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/turbopack/crates/turbopack-node/src/pool_stats.rs b/turbopack/crates/turbopack-node/src/pool_stats.rs index 1286b8e5b832..4634c029b93e 100644 --- a/turbopack/crates/turbopack-node/src/pool_stats.rs +++ b/turbopack/crates/turbopack-node/src/pool_stats.rs @@ -1,7 +1,9 @@ use std::{cmp::max, fmt::Debug, time::Duration}; +#[cfg(any(feature = "process_pool", feature = "worker_pool"))] use tokio::sync::OwnedSemaphorePermit; +#[cfg(any(feature = "process_pool", feature = "worker_pool"))] pub enum AcquiredPermits { Idle { // This is used for drop From ef6e23fa70f5788340ab507b25eea7c6bdb16e60 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:06:43 +0200 Subject: [PATCH 5/9] Turbopack: don't replace single-arg calls with argument in analyzer (#95277) This was a hack added a long time ago. And it can lead to correctness issues --- Cargo.lock | 1 + .../src/analyzer/well_known/mod.rs | 13 ------------- .../env-var-info/fn-map/env-vars.codegen.snapshot | 4 +--- .../env-var-info/fn-map/env-vars.snapshot | 4 +--- .../env-var-info/fn-map/env-vars.tracing.snapshot | 4 +--- .../tests/references/env-var-info/fn-map/input.js | 2 +- 6 files changed, 5 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 998da60da075..bced8e223d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10145,6 +10145,7 @@ dependencies = [ "mockito", "quick_cache", "reqwest", + "rustc-hash 2.1.1", "rustls", "serde", "tokio", diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs index 1f5dfaa2f0b7..8bc1bbe46da9 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/well_known/mod.rs @@ -42,19 +42,6 @@ pub async fn replace_well_known<'a>( Modified::Yes, ) } - JsValue::Call(total, call) => { - // var fs = require('fs'), fs = __importStar(fs); - // TODO(WEB-552) this is not correct and has many false positives! - if call.args().len() == 1 - && let JsValue::WellKnownObject(_) = &call.args()[0] - { - return Ok(( - call.args()[0].clone_in(arena.get_or_default()), - Modified::Yes, - )); - } - (JsValue::Call(total, call), Modified::No) - } JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::WellKnownObject(_)) => { let JsValue::WellKnownObject(kind) = take(&mut *obj) else { unreachable!() diff --git a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.codegen.snapshot b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.codegen.snapshot index 9b3527dc3e6c..e08877920854 100644 --- a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.codegen.snapshot +++ b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.codegen.snapshot @@ -1,3 +1 @@ -runtime: [ - "DECOY", -] +runtime: [] diff --git a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.snapshot b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.snapshot index 9b3527dc3e6c..e08877920854 100644 --- a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.snapshot +++ b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.snapshot @@ -1,3 +1 @@ -runtime: [ - "DECOY", -] +runtime: [] diff --git a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.tracing.snapshot b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.tracing.snapshot index 9b3527dc3e6c..e08877920854 100644 --- a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.tracing.snapshot +++ b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/env-vars.tracing.snapshot @@ -1,3 +1 @@ -runtime: [ - "DECOY", -] +runtime: [] diff --git a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/input.js b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/input.js index ff0c821119a6..bd9624ab8d72 100644 --- a/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/input.js +++ b/turbopack/crates/turbopack-ecmascript/tests/references/env-var-info/fn-map/input.js @@ -1,5 +1,5 @@ +// TODO track arguments and mark FOOBAR as read globalThis.blackbox = (v) => ({ DECOY: v.FOOBAR }) -// TODO fix this const v = blackbox(process.env).DECOY console.log(v) From 529d290b8ff7ccc8faf6cc1e625fbd20976fcf44 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:07:14 +0200 Subject: [PATCH 6/9] [test] Unpin Vercel CLI version (#97974) Reverts #97960 vercel@59.7.0 should fix this again --- test/production/deterministic-build/deployment-id.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/production/deterministic-build/deployment-id.test.ts b/test/production/deterministic-build/deployment-id.test.ts index bd0617ee2d53..5d1f80ef9ed1 100644 --- a/test/production/deterministic-build/deployment-id.test.ts +++ b/test/production/deterministic-build/deployment-id.test.ts @@ -242,8 +242,7 @@ async function runTest( }, // We use NEXT_TEST_PREFER_OFFLINE, so just declaring `vercel: latest` as a dependency still // doesn't force the latest version. - // TODO: Go back to integration testing with latest once breakage in 59.6.2 is resolved. - buildCommand: 'pnpm dlx vercel@59.5.0 build', + buildCommand: 'pnpm dlx vercel@latest build', env: mode === 'adapter' ? { From 286fcc346f4930d01d10003c0708ca4861c692f9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:14:32 +0200 Subject: [PATCH 7/9] Turbopack: call loadActionManifest for app-route (#97921) For #95233, I need to read server-reference-manifest.json even for App Routes, which previously didn't emit server action manifest entries at all (because thus far, it was unnecessary). --- crates/next-api/src/app.rs | 27 +++++++++++-------- crates/next-api/src/operation.rs | 8 +++++- crates/next-api/src/project.rs | 1 + crates/next-api/src/route.rs | 1 + crates/next-api/src/server_actions.rs | 7 ++++- crates/next-build-test/src/lib.rs | 1 + .../src/next_api/project.rs | 4 +++ packages/next/src/build/handle-entrypoints.ts | 3 +++ .../next/src/build/swc/generated-native.d.ts | 1 + packages/next/src/build/swc/index.ts | 2 ++ packages/next/src/build/swc/types.ts | 2 ++ .../next/src/server/dev/turbopack-utils.ts | 3 +++ 12 files changed, 47 insertions(+), 13 deletions(-) diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index dcc8d2289ecd..357c33564706 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -1090,18 +1090,23 @@ pub fn app_entry_point_to_route( } .resolved_cell(), ), + has_action_manifest: true, }, - AppEntrypoint::AppMetadata { page, metadata, .. } => Route::AppRoute { - original_name: page.to_string().into(), - endpoint: ResolvedVc::upcast( - AppEndpoint { - ty: AppEndpointType::Metadata { metadata }, - app_project, - page, - } - .resolved_cell(), - ), - }, + AppEntrypoint::AppMetadata { page, metadata, .. } => { + let has_action_manifest = matches!(metadata, MetadataItem::Dynamic { .. }); + Route::AppRoute { + original_name: page.to_string().into(), + endpoint: ResolvedVc::upcast( + AppEndpoint { + ty: AppEndpointType::Metadata { metadata }, + app_project, + page, + } + .resolved_cell(), + ), + has_action_manifest, + } + } } .cell() } diff --git a/crates/next-api/src/operation.rs b/crates/next-api/src/operation.rs index c37c9812e138..23856883200a 100644 --- a/crates/next-api/src/operation.rs +++ b/crates/next-api/src/operation.rs @@ -100,9 +100,14 @@ fn pick_route(entrypoints: OperationVc, key: RcStr, route: &Route) }) .collect(), ), - Route::AppRoute { original_name, .. } => RouteOperation::AppRoute { + Route::AppRoute { + original_name, + has_action_manifest, + .. + } => RouteOperation::AppRoute { original_name: original_name.clone(), endpoint: pick_endpoint(entrypoints, EndpointSelector::RouteAppRoute(key)), + has_action_manifest: *has_action_manifest, }, Route::Conflict => RouteOperation::Conflict, } @@ -220,6 +225,7 @@ pub enum RouteOperation { AppRoute { original_name: RcStr, endpoint: OperationVc, + has_action_manifest: bool, }, Conflict, } diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 425c54886d6b..20b8200c0a67 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -1370,6 +1370,7 @@ impl Project { Route::AppRoute { original_name: _, endpoint, + .. } => { endpoint_groups.push(( EndpointGroupKey::Route(key.clone()), diff --git a/crates/next-api/src/route.rs b/crates/next-api/src/route.rs index 8a2ba2b5fbbb..2aecbb5ce807 100644 --- a/crates/next-api/src/route.rs +++ b/crates/next-api/src/route.rs @@ -38,6 +38,7 @@ pub enum Route { AppRoute { original_name: RcStr, endpoint: ResolvedVc>, + has_action_manifest: bool, }, Conflict, } diff --git a/crates/next-api/src/server_actions.rs b/crates/next-api/src/server_actions.rs index f5f40d2c43d4..4be69efd26cf 100644 --- a/crates/next-api/src/server_actions.rs +++ b/crates/next-api/src/server_actions.rs @@ -784,7 +784,12 @@ pub async fn map_server_actions( .map(async |module| { // TODO: compare module contexts instead? let layer = match module.ident().await?.layer.as_ref() { - Some(layer) if layer.name() == "app-rsc" || layer.name() == "app-edge-rsc" => { + Some(layer) + if layer.name() == "app-rsc" + || layer.name() == "app-edge-rsc" + || layer.name() == "app-route" + || layer.name() == "app-edge-route" => + { ActionLayer::Rsc } Some(layer) if layer.name() == "app-client" => ActionLayer::ActionBrowser, diff --git a/crates/next-build-test/src/lib.rs b/crates/next-build-test/src/lib.rs index e9ea3a4776cb..7d9ae2740541 100644 --- a/crates/next-build-test/src/lib.rs +++ b/crates/next-build-test/src/lib.rs @@ -204,6 +204,7 @@ pub async fn render_routes( Route::AppRoute { original_name: _, endpoint, + .. } => { endpoint_write_to_disk_with_apply(endpoint).await?; } diff --git a/crates/next-napi-bindings/src/next_api/project.rs b/crates/next-napi-bindings/src/next_api/project.rs index 3a19cc9628df..a7888dbae2f9 100644 --- a/crates/next-napi-bindings/src/next_api/project.rs +++ b/crates/next-napi-bindings/src/next_api/project.rs @@ -841,6 +841,8 @@ pub struct NapiRoute { pub pages: Option>, + pub has_action_manifest: Option, + // Different representations of the endpoint pub endpoint: Option>, pub html_endpoint: Option>, @@ -895,11 +897,13 @@ impl NapiRoute { RouteOperation::AppRoute { original_name, endpoint, + has_action_manifest, } => NapiRoute { pathname, original_name: Some(original_name), r#type: "app-route", endpoint: convert_endpoint(endpoint), + has_action_manifest: Some(has_action_manifest), ..Default::default() }, RouteOperation::Conflict => NapiRoute { diff --git a/packages/next/src/build/handle-entrypoints.ts b/packages/next/src/build/handle-entrypoints.ts index 60550c6d417f..c8cf785bfd69 100644 --- a/packages/next/src/build/handle-entrypoints.ts +++ b/packages/next/src/build/handle-entrypoints.ts @@ -118,6 +118,9 @@ export async function handleRouteType({ const key = getEntryKey('app', 'server', page) manifestLoader.loadAppPathsManifest(page) + if (route.hasActionManifest) { + manifestLoader.loadActionManifest(page) + } const middlewareManifestWritten = manifestLoader.loadMiddlewareManifest( page, diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index 69ad05307c38..acea8b1fb86b 100644 --- a/packages/next/src/build/swc/generated-native.d.ts +++ b/packages/next/src/build/swc/generated-native.d.ts @@ -487,6 +487,7 @@ export interface NapiRoute { /** The type of route, eg a Page or App */ type: string pages?: Array + hasActionManifest?: boolean endpoint?: ExternalObject htmlEndpoint?: ExternalObject rscHmrEndpoint?: ExternalObject diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 8484b3fd0d0d..0be00aa9e3f9 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -551,6 +551,7 @@ function bindingToApi( | { type: 'app-route' originalName: string + hasActionManifest: boolean endpoint: NapiEndpoint } | { @@ -1205,6 +1206,7 @@ function bindingToApi( route = { type: 'app-route', originalName: nativeRoute.originalName, + hasActionManifest: nativeRoute.hasActionManifest, endpoint: new EndpointImpl(nativeRoute.endpoint), } break diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 6ffbaaeab4ab..0c15c33039de 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -384,6 +384,7 @@ export type Route = | { type: 'app-route' originalName: string + hasActionManifest: boolean endpoint: Endpoint } | { @@ -528,6 +529,7 @@ export type AppRoute = } | { type: 'app-route' + hasActionManifest: boolean endpoint: Endpoint } diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts index 0c9b400a5219..8f28c208e1ba 100644 --- a/packages/next/src/server/dev/turbopack-utils.ts +++ b/packages/next/src/server/dev/turbopack-utils.ts @@ -460,6 +460,9 @@ export async function handleRouteType({ const type = writtenEndpoint.type manifestLoader.loadAppPathsManifest(page) + if (route.hasActionManifest) { + manifestLoader.loadActionManifest(page) + } if (type === 'edge') { warnAboutEdgeRuntime() From 89d017eac43ebf4742ed2b58d3c1373b70283b69 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 27 Aug 2026 13:00:17 +0200 Subject: [PATCH 8/9] Pages Router: Deprecate React 18 support (#97689) ## Summary - warn during `next dev` and `next build` when React 18 is installed, while keeping `next start` quiet - explain that React 18 remains supported in Next.js 16 but will be removed in Next.js 17 - update the React version guidance with React 19 and TypeScript type upgrade commands ## Verification - `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/e2e/react-current-version/react-current-version.test.ts` - `NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/e2e/react-current-version/react-current-version.test.ts` - targeted production build/start coverage passed with React 18 and React 19 - Not passing: `pnpm --filter=next types` (existing unrelated errors in `httpget.ts`, `image-optimizer.ts`, `proxy-request.ts`, telemetry typings, and compiled declarations) - Not passing: `pnpm lint` (existing unused Turbo Tasks check) --- errors/react-version.mdx | 60 +++++++++++-------- packages/next/src/server/config.ts | 35 +++++++++++ .../deprecation-warnings.test.ts | 12 ++-- .../react-current-version.test.ts | 45 ++++++++++++++ .../build-output-prerender.test.ts | 5 ++ 5 files changed, 127 insertions(+), 30 deletions(-) diff --git a/errors/react-version.mdx b/errors/react-version.mdx index 80603e43a2fa..0e5284481500 100644 --- a/errors/react-version.mdx +++ b/errors/react-version.mdx @@ -1,53 +1,61 @@ --- -title: Minimum React Version +title: React Version Support +description: Learn which React versions are supported by Next.js and how to upgrade. --- ## Why This Error Occurred -Your project is using an old version of `react` or `react-dom` that does not -meet the suggested minimum version requirement. +Your project is using a version of `react` or `react-dom` that is unsupported or +deprecated in your version of Next.js. -Next.js suggests using, at a minimum, `react@18.2.0` and `react-dom@18.2.0`. -Older versions of `react` and `react-dom` do work with Next.js, however, they do -not enable all of Next.js' features. +- React versions earlier than `18.2.0` are unsupported. +- React 18.2 and later remain supported in Next.js 16, but React 18 support is + deprecated. +- React 19 is recommended for Next.js 16 and will be required in Next.js 17. -For example, the following features are not enabled with old React versions: +React 19 includes features used by Next.js, such as Actions and improved +hydration errors. Upgrade `react` and `react-dom` together to keep their versions +in sync. -- [Fast Refresh](/docs/architecture/fast-refresh): instantly - view edits to your app without losing component state -- Component stack trace in development: see the component tree that lead up to - an error -- Hydration mismatch warnings: trace down discrepancies in your React tree that - cause performance problems - -This list is not exhaustive, but illustrative in the value of upgrading React! +If you use TypeScript, upgrade `@types/react` and `@types/react-dom` at the same +time. ## Possible Ways to Fix It -**Via npm** +Upgrade React and its TypeScript types with your package manager: + +```bash package="npm" +npm install react@latest react-dom@latest @types/react@latest @types/react-dom@latest +``` -```bash filename="Terminal" -npm upgrade react@latest react-dom@latest +```bash package="yarn" +yarn add react@latest react-dom@latest @types/react@latest @types/react-dom@latest ``` -**Via Yarn** +```bash package="pnpm" +pnpm add react@latest react-dom@latest @types/react@latest @types/react-dom@latest +``` -```bash filename="Terminal" -yarn add react@latest react-dom@latest +```bash package="bun" +bun add react@latest react-dom@latest @types/react@latest @types/react-dom@latest ``` -**Manually** Open your `package.json` and upgrade `react` and `react-dom`: +Alternatively, update your `package.json` manually: ```json filename="package.json" { "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" } } ``` ## Useful Links -- [Fast Refresh blog post](/blog/next-9-4#fast-refresh) -- [Fast Refresh docs](/docs/architecture/fast-refresh) +- [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) +- [Upgrading Next.js](/docs/app/getting-started/upgrading) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 6858f8d3056c..a5bc906788a9 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1,4 +1,5 @@ import { existsSync } from 'fs' +import { createRequire } from 'module' import { basename, extname, join, relative, isAbsolute, resolve } from 'path' import { pathToFileURL } from 'url' import findUp from 'next/dist/compiled/find-up' @@ -61,6 +62,38 @@ import { hrtimeBigIntDurationToString } from '../build/duration-to-string' export { normalizeConfig } from './config-shared' export type { DomainLocale, NextConfig } from './config-shared' +const REACT_18_DEPRECATION_WARNING = + 'React 18 support is deprecated in Next.js 16 and will be removed in Next.js 17. Please upgrade to React 19. Learn more: https://nextjs.org/docs/messages/react-version' + +function getInstalledPackageVersion(dir: string, name: 'react' | 'react-dom') { + try { + const projectRequire = createRequire(join(dir, 'package.json')) + return (projectRequire(name) as { version?: string }).version + } catch { + return undefined + } +} + +function warnIfReact18IsInstalled( + phase: PHASE_TYPE, + dir: string, + silent: boolean | undefined +) { + if ( + silent !== false || + (phase !== PHASE_DEVELOPMENT_SERVER && phase !== PHASE_PRODUCTION_BUILD) + ) { + return + } + + const reactVersion = getInstalledPackageVersion(dir, 'react') + const reactDomVersion = getInstalledPackageVersion(dir, 'react-dom') + + if (reactVersion?.startsWith('18.') || reactDomVersion?.startsWith('18.')) { + Log.warnOnce(REACT_18_DEPRECATION_WARNING) + } +} + function normalizeNextConfigZodErrors( error: ZodError ): [warnings: string[], fatalErrors: string[]] { @@ -1814,6 +1847,8 @@ export default async function loadConfig( const startTimeNanos = logTiming ? process.hrtime.bigint() : undefined const [config, meta] = await loadConfigImpl(phase, dir, opts) + warnIfReact18IsInstalled(phase, dir, opts.silent) + if (!meta.cacheHit && logTiming) { const durationNanos = process.hrtime.bigint() - startTimeNanos! Log.event( diff --git a/test/e2e/deprecation-warnings/deprecation-warnings.test.ts b/test/e2e/deprecation-warnings/deprecation-warnings.test.ts index b7bee6077810..5e3734c2aefa 100644 --- a/test/e2e/deprecation-warnings/deprecation-warnings.test.ts +++ b/test/e2e/deprecation-warnings/deprecation-warnings.test.ts @@ -12,10 +12,14 @@ describe('deprecation-warnings', () => { it('should not emit any deprecation warnings when no config file exists', async () => { await next.start() - const logs = next.cliOutput - expect(logs).not.toContain('deprecated') - expect(logs).not.toContain('has been renamed') - expect(logs).not.toContain('no longer needed') + // React version warnings are covered by the dedicated React version test. + const configLogs = next.cliOutput + .split('\n') + .filter((line) => !line.includes('React 18 support is deprecated')) + .join('\n') + expect(configLogs).not.toContain('deprecated') + expect(configLogs).not.toContain('has been renamed') + expect(configLogs).not.toContain('no longer needed') }) }) diff --git a/test/e2e/react-current-version/react-current-version.test.ts b/test/e2e/react-current-version/react-current-version.test.ts index dbd42b3c298b..1a2a3d9eb087 100644 --- a/test/e2e/react-current-version/react-current-version.test.ts +++ b/test/e2e/react-current-version/react-current-version.test.ts @@ -2,6 +2,9 @@ import { isReact18, nextTestSetup, isNextDev } from 'e2e-utils' import { join } from 'path' import { retry } from 'next-test-utils' +const react18DeprecationWarning = + 'React 18 support is deprecated in Next.js 16 and will be removed in Next.js 17. Please upgrade to React 19. Learn more: https://nextjs.org/docs/messages/react-version' + function makeIndexPage(runtime: string) { return `import ReactDOM from 'react-dom' import Image from 'next/image' @@ -32,6 +35,48 @@ export const config = { } describe('react-current-version', () => { + describe('React 18 deprecation warning', () => { + const { next, isNextDeploy, skipped } = nextTestSetup({ + files: join(__dirname, 'app'), + skipStart: true, + }) + + if (skipped) { + return + } + + if (isNextDeploy) { + it('should skip in deploy mode', () => {}) + return + } + + if (isNextDev) { + it('warns during next dev only when using React 18', async () => { + await next.start() + await retry(() => { + expect(next.cliOutput).toContain('Running next.config') + }) + + const warningCount = + next.cliOutput.split(react18DeprecationWarning).length - 1 + expect(warningCount).toBe(isReact18 ? 1 : 0) + }) + } else { + it('warns during next build only when using React 18 but not during next start', async () => { + const buildResult = await next.build() + const warningCount = + buildResult.cliOutput.split(react18DeprecationWarning).length - 1 + expect(warningCount).toBe(isReact18 ? 1 : 0) + + const outputIndex = next.cliOutput.length + await next.start() + expect(next.cliOutput.slice(outputIndex)).not.toContain( + react18DeprecationWarning + ) + }) + } + }) + describe('Basics', () => { const { next, isTurbopack } = nextTestSetup({ files: join(__dirname, 'app'), diff --git a/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts b/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts index 91b8a7b10be3..bcad5a4ae85b 100644 --- a/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts +++ b/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts @@ -489,6 +489,11 @@ function getPreambleOutput(cliOutput: string): string { const lines: string[] = [] for (const line of cliOutput.split('\n')) { + // React version warnings are covered by the dedicated React version test. + if (line.includes('React 18 support is deprecated')) { + continue + } + if (line.includes('Creating an optimized production build')) { break } From 3d85af96be3953a336e887a665d74f021eed443f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:42:02 +0200 Subject: [PATCH 9/9] Expose durableUseCacheEntries config in workStore (#97926) https://github.com/vercel/next.js/pull/95233 needs this to decide whether to perform the manifest lookup. --- packages/next/src/build/index.ts | 6 ++++ packages/next/src/build/static-paths/app.ts | 3 ++ .../src/build/templates/app-page-runtime.ts | 3 ++ .../next/src/build/templates/app-route.ts | 3 ++ .../next/src/build/templates/edge-ssr-app.ts | 3 ++ packages/next/src/build/utils.ts | 3 ++ packages/next/src/export/index.ts | 3 ++ packages/next/src/export/routes/app-route.ts | 5 ++- .../next/src/server/app-render/app-render.tsx | 3 ++ .../dev-validation-worker-globals.ts | 1 + packages/next/src/server/app-render/types.ts | 1 + .../app-render/work-async-storage.external.ts | 1 + .../src/server/async-storage/work-store.ts | 6 +++- packages/next/src/server/base-server.ts | 3 ++ packages/next/src/server/config-shared.ts | 2 ++ packages/next/src/server/config.test.ts | 36 +++++++++++++++++++ packages/next/src/server/config.ts | 7 ++++ .../server/dev/dev-validation-worker-pool.ts | 3 ++ .../next/src/server/dev/next-dev-server.ts | 3 ++ .../src/server/dev/static-paths-worker.ts | 3 ++ .../src/server/dev/use-cache-probe-pool.ts | 3 ++ .../src/server/dev/use-cache-probe-worker.ts | 2 ++ packages/next/src/server/web/adapter.ts | 1 + .../server/web/edge-route-module-wrapper.ts | 1 + 24 files changed, 103 insertions(+), 2 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 309ecede7186..83c332aa21b5 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -2285,6 +2285,9 @@ export default async function build( cacheComponents: isAppCacheComponentsEnabled, authInterrupts: isAuthInterruptsEnabled, useCacheTimeout: config.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + config.experimental.durableUseCacheEntries + ), staticPageGenerationTimeout: config.staticPageGenerationTimeout, httpAgentOptions: config.httpAgentOptions, locales: config.i18n?.locales, @@ -2517,6 +2520,9 @@ export default async function build( authInterrupts: isAuthInterruptsEnabled, useCacheTimeout: config.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + config.experimental.durableUseCacheEntries + ), staticPageGenerationTimeout: config.staticPageGenerationTimeout, cacheHandler: config.cacheHandler, diff --git a/packages/next/src/build/static-paths/app.ts b/packages/next/src/build/static-paths/app.ts index 5ad4c6e12c5a..6f7186aabcba 100644 --- a/packages/next/src/build/static-paths/app.ts +++ b/packages/next/src/build/static-paths/app.ts @@ -823,6 +823,7 @@ export async function buildAppStaticPaths({ cacheComponents, authInterrupts, useCacheTimeout, + durableUseCacheEntries, staticPageGenerationTimeout, segments, isrFlushToDisk, @@ -845,6 +846,7 @@ export async function buildAppStaticPaths({ cacheComponents: boolean authInterrupts: boolean useCacheTimeout: number + durableUseCacheEntries: boolean staticPageGenerationTimeout: number segments: readonly Readonly[] distDir: string @@ -910,6 +912,7 @@ export async function buildAppStaticPaths({ experimental: { authInterrupts, useCacheTimeout, + durableUseCacheEntries, }, waitUntil: afterRunner.context.waitUntil, onClose: afterRunner.context.onClose, diff --git a/packages/next/src/build/templates/app-page-runtime.ts b/packages/next/src/build/templates/app-page-runtime.ts index e2af1eea0185..1e34a96c38a0 100644 --- a/packages/next/src/build/templates/app-page-runtime.ts +++ b/packages/next/src/build/templates/app-page-runtime.ts @@ -948,6 +948,9 @@ export function createAppPageEntrypoint({ nextConfig.experimental.serverComponentsHmrCancellation ), useCacheTimeout: nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + nextConfig.experimental.durableUseCacheEntries + ), cachedNavigations: nextConfig.experimental.cachedNavigations ?? false, clientTraceMetadata: diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index cb13c334a433..ef6224a46061 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -237,6 +237,9 @@ export async function handler( experimental: { authInterrupts: Boolean(nextConfig.experimental.authInterrupts), useCacheTimeout: nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + nextConfig.experimental.durableUseCacheEntries + ), }, cacheComponents: Boolean(nextConfig.cacheComponents), validationLevel: nextConfig.experimental.instantInsights.validationLevel, diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 969373c59b3f..474d85a1235f 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -180,6 +180,9 @@ async function requestHandler( // no-op. serverComponentsHmrCancellation: false, useCacheTimeout: nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + nextConfig.experimental.durableUseCacheEntries + ), cachedNavigations: nextConfig.experimental.cachedNavigations ?? false, clientTraceMetadata: nextConfig.experimental.clientTraceMetadata || ([] as any), diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index cb850f904967..1fcd611ef20e 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -699,6 +699,7 @@ export async function isPageStatic({ cacheComponents, authInterrupts, useCacheTimeout, + durableUseCacheEntries, staticPageGenerationTimeout, originalAppPath, isrFlushToDisk, @@ -718,6 +719,7 @@ export async function isPageStatic({ cacheComponents: boolean authInterrupts: boolean useCacheTimeout: number + durableUseCacheEntries: boolean staticPageGenerationTimeout: number configFileName: string httpAgentOptions: NextConfigComplete['httpAgentOptions'] @@ -904,6 +906,7 @@ export async function isPageStatic({ cacheComponents, authInterrupts, useCacheTimeout, + durableUseCacheEntries, staticPageGenerationTimeout, segments, distDir, diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 8a7947724354..ee3cac107dc6 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -525,6 +525,9 @@ async function exportAppImpl( authInterrupts: !!nextConfig.experimental.authInterrupts, reactBrowserBailout: nextConfig.experimental.reactBrowserBailout ?? false, useCacheTimeout: nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + nextConfig.experimental.durableUseCacheEntries + ), cachedNavigations: nextConfig.experimental.cachedNavigations ?? false, maxPostponedStateSizeBytes: parseMaxPostponedStateSize( nextConfig.experimental.maxPostponedStateSize diff --git a/packages/next/src/export/routes/app-route.ts b/packages/next/src/export/routes/app-route.ts index 9f4f4cc8217f..32db576865c1 100644 --- a/packages/next/src/export/routes/app-route.ts +++ b/packages/next/src/export/routes/app-route.ts @@ -46,7 +46,10 @@ export async function exportAppRoute( cacheComponents: boolean, staticPageGenerationTimeout: number, experimental: Required< - Pick + Pick< + ExperimentalConfig, + 'authInterrupts' | 'useCacheTimeout' | 'durableUseCacheEntries' + > >, buildId: string, deploymentId: string diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 459847b650bf..27e19ac968fe 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -6577,6 +6577,8 @@ function buildDevValidationWorkStore( forceStatic: message.forceStatic, isDraftMode: message.request.isDraftMode, useCacheTimeout: message.nextConfigSerializable.useCacheTimeout, + durableUseCacheEntries: + message.nextConfigSerializable.durableUseCacheEntries, staticPageGenerationTimeout: message.nextConfigSerializable.staticPageGenerationTimeout, cacheLifeProfiles: message.nextConfigSerializable.cacheLifeProfiles, @@ -8224,6 +8226,7 @@ async function validateInstantConfigInBuildWithSample( incrementalCache: outerWorkStore.incrementalCache, cacheLifeProfiles: outerWorkStore.cacheLifeProfiles, useCacheTimeout: outerWorkStore.useCacheTimeout, + durableUseCacheEntries: outerWorkStore.durableUseCacheEntries, staticPageGenerationTimeout: outerWorkStore.staticPageGenerationTimeout, isBuildTimePrerendering: false, fetchCache: outerWorkStore.fetchCache, diff --git a/packages/next/src/server/app-render/dev-validation-worker-globals.ts b/packages/next/src/server/app-render/dev-validation-worker-globals.ts index 21a585511bb7..01b537aa2516 100644 --- a/packages/next/src/server/app-render/dev-validation-worker-globals.ts +++ b/packages/next/src/server/app-render/dev-validation-worker-globals.ts @@ -122,6 +122,7 @@ export interface DevValidationInstallFields { httpAgentOptions: NextConfigComplete['httpAgentOptions'] cacheLifeProfiles: NextConfigComplete['cacheLife'] useCacheTimeout: number + durableUseCacheEntries: boolean staticPageGenerationTimeout: number } } diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 039f197eaa4d..252d7b2be3ee 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -169,6 +169,7 @@ export interface RenderOptsPartial { reactBrowserBailout: boolean serverComponentsHmrCancellation?: boolean useCacheTimeout: number + durableUseCacheEntries: boolean cachedNavigations: boolean /** diff --git a/packages/next/src/server/app-render/work-async-storage.external.ts b/packages/next/src/server/app-render/work-async-storage.external.ts index 765757246246..218c351334e3 100644 --- a/packages/next/src/server/app-render/work-async-storage.external.ts +++ b/packages/next/src/server/app-render/work-async-storage.external.ts @@ -29,6 +29,7 @@ export interface WorkStore { readonly incrementalCache?: IncrementalCache readonly cacheLifeProfiles: ResolvedCacheLifeProfiles readonly useCacheTimeout: number + readonly durableUseCacheEntries: boolean readonly staticPageGenerationTimeout: number readonly isOnDemandRevalidate?: boolean diff --git a/packages/next/src/server/async-storage/work-store.ts b/packages/next/src/server/async-storage/work-store.ts index eca01c0cc345..c6b405609fd0 100644 --- a/packages/next/src/server/async-storage/work-store.ts +++ b/packages/next/src/server/async-storage/work-store.ts @@ -41,7 +41,10 @@ export type WorkStoreContext = { pendingWaitUntil?: Promise experimental: Pick< RenderOpts['experimental'], - 'isRoutePPREnabled' | 'authInterrupts' | 'useCacheTimeout' + | 'isRoutePPREnabled' + | 'authInterrupts' + | 'useCacheTimeout' + | 'durableUseCacheEntries' > /** @@ -119,6 +122,7 @@ function createWorkStoreImpl( renderOpts.incrementalCache || (globalThis as any).__incrementalCache, cacheLifeProfiles: renderOpts.cacheLifeProfiles, useCacheTimeout: renderOpts.experimental.useCacheTimeout, + durableUseCacheEntries: renderOpts.experimental.durableUseCacheEntries, staticPageGenerationTimeout: renderOpts.staticPageGenerationTimeout, isBuildTimePrerendering: renderOpts.isBuildTimePrerendering, fetchCache: renderOpts.fetchCache, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index befa70e40205..790a91e63047 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -614,6 +614,9 @@ export default abstract class Server< serverComponentsHmrCancellation: this.nextConfig.experimental.serverComponentsHmrCancellation, useCacheTimeout: this.nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + this.nextConfig.experimental.durableUseCacheEntries + ), cachedNavigations: this.nextConfig.experimental.cachedNavigations ?? false, maxPostponedStateSizeBytes: parseMaxPostponedStateSize( diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 385eab6a7fa0..dba9fc6603a9 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -2430,6 +2430,7 @@ export interface NextConfigRuntime { | 'authInterrupts' | 'reactBrowserBailout' | 'useCacheTimeout' + | 'durableUseCacheEntries' | 'clientTraceMetadata' | 'clientParamParsingOrigins' | 'allowedRevalidateHeaderKeys' @@ -2498,6 +2499,7 @@ export function getNextConfigRuntime( authInterrupts: ex.authInterrupts, reactBrowserBailout: ex.reactBrowserBailout, useCacheTimeout: ex.useCacheTimeout, + durableUseCacheEntries: ex.durableUseCacheEntries, clientTraceMetadata: ex.clientTraceMetadata, clientParamParsingOrigins: ex.clientParamParsingOrigins, allowedRevalidateHeaderKeys: ex.allowedRevalidateHeaderKeys, diff --git a/packages/next/src/server/config.test.ts b/packages/next/src/server/config.test.ts index 4f183e0a6489..da36e17924c3 100644 --- a/packages/next/src/server/config.test.ts +++ b/packages/next/src/server/config.test.ts @@ -242,4 +242,40 @@ describe('loadConfig', () => { expect(result.experimental.cssChunking).toBe('graph') }) }) + + describe('experimental.durableUseCacheEntries', () => { + const originalTurbopack = process.env.TURBOPACK + + afterEach(() => { + if (originalTurbopack === undefined) { + delete process.env.TURBOPACK + } else { + process.env.TURBOPACK = originalTurbopack + } + }) + + it('throws when using webpack', async () => { + delete process.env.TURBOPACK + + await expect( + loadConfig(PHASE_PRODUCTION_BUILD, __dirname, { + customConfig: { + experimental: { durableUseCacheEntries: true }, + }, + }) + ).rejects.toThrow(/only supported with Turbopack/) + }) + + it('is preserved when using Turbopack', async () => { + process.env.TURBOPACK = '1' + + const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, { + customConfig: { + experimental: { durableUseCacheEntries: true }, + }, + }) + + expect(result.experimental.durableUseCacheEntries).toBe(true) + }) + }) }) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index a5bc906788a9..2fa3dcfbf01d 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -548,6 +548,13 @@ function assignDefaultsAndValidate( // Only validate during build/dev — `next start` doesn't pick a bundler and would otherwise // see `process.env.TURBOPACK` unset and reject a valid `cssChunking: "graph"` config. if (phase !== PHASE_PRODUCTION_SERVER && phase !== PHASE_INFO) { + if (result.experimental.durableUseCacheEntries && !process.env.TURBOPACK) { + throw new Error( + `\`experimental.durableUseCacheEntries: true\` is only supported with Turbopack. ` + + `Please remove the option or run Next.js with Turbopack in ${configFileName}.` + ) + } + const cssChunkingValue = result.experimental.cssChunking const cssChunkingMode = resolveCssChunkingMode(cssChunkingValue) if (cssChunkingMode === 'graph' && !process.env.TURBOPACK) { diff --git a/packages/next/src/server/dev/dev-validation-worker-pool.ts b/packages/next/src/server/dev/dev-validation-worker-pool.ts index 7acff8f96c5f..92e9f9719821 100644 --- a/packages/next/src/server/dev/dev-validation-worker-pool.ts +++ b/packages/next/src/server/dev/dev-validation-worker-pool.ts @@ -222,6 +222,9 @@ export function installDevValidationWorker(options: InstallOptions): void { httpAgentOptions: nextConfig.httpAgentOptions, cacheLifeProfiles: nextConfig.cacheLife, useCacheTimeout: nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + nextConfig.experimental.durableUseCacheEntries + ), staticPageGenerationTimeout: nextConfig.staticPageGenerationTimeout, }, } diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index 977f8fc73ee7..f67f33d41c01 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -772,6 +772,9 @@ export default class DevServer extends Server { deploymentId: this.deploymentId, authInterrupts: Boolean(this.nextConfig.experimental.authInterrupts), useCacheTimeout: this.nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + this.nextConfig.experimental.durableUseCacheEntries + ), staticPageGenerationTimeout: this.nextConfig.staticPageGenerationTimeout, sriEnabled: Boolean(this.nextConfig.experimental.sri?.algorithm), diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 6f8297946142..119e9557f806 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -54,6 +54,7 @@ export async function loadStaticPaths({ deploymentId, authInterrupts, useCacheTimeout, + durableUseCacheEntries, staticPageGenerationTimeout, sriEnabled, }: { @@ -78,6 +79,7 @@ export async function loadStaticPaths({ deploymentId: string authInterrupts: boolean useCacheTimeout: number + durableUseCacheEntries: boolean staticPageGenerationTimeout: number sriEnabled: boolean }): Promise { @@ -149,6 +151,7 @@ export async function loadStaticPaths({ deploymentId, authInterrupts, useCacheTimeout, + durableUseCacheEntries, staticPageGenerationTimeout, rootParamKeys, }) diff --git a/packages/next/src/server/dev/use-cache-probe-pool.ts b/packages/next/src/server/dev/use-cache-probe-pool.ts index 09274b95a76c..10ff10b65e75 100644 --- a/packages/next/src/server/dev/use-cache-probe-pool.ts +++ b/packages/next/src/server/dev/use-cache-probe-pool.ts @@ -187,6 +187,9 @@ export function installUseCacheProbe(options: InstallOptions): void { httpAgentOptions: nextConfig.httpAgentOptions, cacheLifeProfiles: nextConfig.cacheLife, useCacheTimeout: nextConfig.experimental.useCacheTimeout, + durableUseCacheEntries: Boolean( + nextConfig.experimental.durableUseCacheEntries + ), staticPageGenerationTimeout: nextConfig.staticPageGenerationTimeout, }, timeoutMs: args.timeoutMs, diff --git a/packages/next/src/server/dev/use-cache-probe-worker.ts b/packages/next/src/server/dev/use-cache-probe-worker.ts index 04ffb98a98ff..3bfde76431fc 100644 --- a/packages/next/src/server/dev/use-cache-probe-worker.ts +++ b/packages/next/src/server/dev/use-cache-probe-worker.ts @@ -57,6 +57,7 @@ export type ProbeMessage = { httpAgentOptions: NextConfigComplete['httpAgentOptions'] cacheLifeProfiles: NextConfigComplete['cacheLife'] useCacheTimeout: number + durableUseCacheEntries: boolean staticPageGenerationTimeout: number } timeoutMs: number @@ -202,6 +203,7 @@ function buildProbeWorkStore(msg: ProbeMessage): WorkStore { useCacheProbeMode: { timeoutMs: msg.timeoutMs }, isDraftMode: msg.request.isDraftMode, useCacheTimeout: msg.nextConfigSerializable.useCacheTimeout, + durableUseCacheEntries: msg.nextConfigSerializable.durableUseCacheEntries, staticPageGenerationTimeout: msg.nextConfigSerializable.staticPageGenerationTimeout, cacheLifeProfiles: msg.nextConfigSerializable.cacheLifeProfiles, diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 4baecf7f5c67..8113b2f2ebc7 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -341,6 +341,7 @@ export async function adapter( // the cache fill will time out immediately and surface the // bug. useCacheTimeout: 0, + durableUseCacheEntries: false, }, waitUntil, onClose: closeController.onClose.bind(closeController), diff --git a/packages/next/src/server/web/edge-route-module-wrapper.ts b/packages/next/src/server/web/edge-route-module-wrapper.ts index 16f942421349..015da7260ef8 100644 --- a/packages/next/src/server/web/edge-route-module-wrapper.ts +++ b/packages/next/src/server/web/edge-route-module-wrapper.ts @@ -132,6 +132,7 @@ export class EdgeRouteModuleWrapper { // never read. 0 is a sentinel: if something ever reads it, the cache // fill will time out immediately and surface the bug. useCacheTimeout: 0, + durableUseCacheEntries: false, }, cacheLifeProfiles: nextConfig.cacheLife, // Edge runtime doesn't do static generation, so this value does not