## Summary
Make deploy mode follow the existing setup/start lifecycle: skipStart
prepares the fixture without deploying, and next.start() performs the
deployment. Failed starts retain CLI and build logs before rejecting, so
tests can assert expected build failures in their own bodies. Default
automatic startup still fails its setup hook on an uncaught deployment
error.
Remove the expectDeploymentFailure option. Refactor
app-invalid-revalidate into fixed fixtures and use the same start/error
assertions locally and on Vercel, without a deployment exclusion.
## Verification
- All four invalid-revalidate cases passed against real Vercel preview
builds with Turbopack. Each test caught the rejection from
`next.start()` and asserted the expected diagnostic in `next.cliOutput`;
no skipped tests.
- All four cases passed locally in dev and start modes with both
Turbopack and webpack.
- Seventeen lifecycle unit tests passed, covering the real
`nextTestSetup` hooks, deferred/default startup, failed-build logs,
retries, repeated starts, custom scripts, existing deployments, and
cleanup.
- `pnpm test-deploy-turbo
test/e2e/test-utils-tests/basic/basic.test.ts`: the default automatic
deployment reached READY and loaded its build/deployment IDs, but the
HTTP assertion failed because the temporary project's protection
redirected to Vercel SSO. Project protection settings were left
unchanged.
The suite's separate, existing Cache Components manifest exclusion is
unchanged.
<!-- NEXT_JS_LLM -->
Stacked on #98643.
This PR adds `experimental.agenticAutoUpgrade = 'future'` config which
enables nudging the agents to notify the user when there's unadopted
future default(s). The nudge will include guiding to upgrade via `next
upgrade --ai` (run "future" by detecting config).
The method of nudging leverages the agents behavior where they tend to
listen to messages from fatal errors that blocks the process compared to
general error/warning logs. Whenever the agents run `next dev` or `next
build`, Next.js will detect the condition and nudge the agent using this
method.
Afterwards it's up to the user whether to proceed the upgrade or not,
it's 100% up to the user how to run it e.g. subagent, background agent,
etc. and the process should not enforce any that affects user's
workflow.
Enabling `experimental.agenticAutoUpgrade = 'future'` also enables
security check and latest version check.
Stacked on #98640.
This PR adds `next upgrade --ai="future"` flag, which is targeted to
help users leverage agents to upgrade their app to adopt the future
defaults when available. Just like latest version upgrade, it covers
running codemods and a migration checklist for major-to-major upgrades
to support breaking changes more reliably.
This PR currently covers Cache Components only for the future default.
Stacked on #98633.
This PR adds `experimental.agenticAutoUpgrade = 'latest'` config which
enables nudging the agents to notify the user when there's new
major/minor Next.js version available to upgrade. The nudge will include
guiding to upgrade via `next upgrade --ai` (run "latest" by detecting
config).
The method of nudging leverages the agents behavior where they tend to
listen to messages from fatal errors that blocks the process compared to
general error/warning logs. Whenever the agents run `next dev` or `next
build`, Next.js will detect the condition and nudge the agent using this
method.
Afterwards it's up to the user whether to proceed the upgrade or not,
it's 100% up to the user how to run it e.g. subagent, background agent,
etc. and the process should not enforce any that affects user's
workflow.
Enabling `experimental.agenticAutoUpgrade = 'latest'` also enables
security check.
Stacked on #98637.
This PR adds `next upgrade --ai="latest"` flag, which is targeted to
help users leverage agents to upgrade their app to the latest major
version when available. Just like security upgrade, it covers running
codemods and a migration checklist for major-to-major upgrades to
support breaking changes more reliably.
Stacked on #98562.
> [!TIP]
> Recommended to review commit by commit.
This PR adds `experimental.agenticAutoUpgrade = 'security'` config which
enables nudging the agents to notify the user when the app's Next.js
version has any security advisories. The nudge will include guiding to
upgrade via `next upgrade --ai` (run "security" by detecting config).
The method of nudging leverages the agents behavior where they tend to
listen to messages from fatal errors that blocks the process compared to
general error/warning logs. Whenever the agents run `next dev` or `next
build`, Next.js will detect the condition and nudge the agent using this
method.
Afterwards it's up to the user whether to proceed the upgrade or not,
it's 100% up to the user how to run it e.g. subagent, background agent,
etc. and the process should not enforce any that affects user's
workflow.
> [!TIP]
> Recommended to review commit by commit.
This PR adds `next upgrade --experimental-ai="security"` flag (alias
`--ai`), which is targeted to help users leverage agents to upgrade
their app to the safe major version when their app's Next.js version has
any security advisories.
Once the command is ran from the user, Next.js will detect the installed
agent harness in user's device, currently limited to Codex and Claude,
and will proceed with starting an agent session once approved. If it is
called within an agent session, the work will continue off within that
agent.
`next upgrade --ai` simply does two things:
- prepare the relevant context to temporary dir
- print hand off prompt, guiding to read those context
The context will guide the agent to run relevant codemods and migration
checklist to proceed. This PR is a base core of the workflow, and will
have wrappers of entry point around this. Also, will add "latest" and
"future" as follow up, which will cover the app to be always latest, and
adopt the future defaults like Cache Components.
This PR also sets up the evals infra and adds evals.
Ported from #98040 by @marcoshernanz.
> [!TIP]
> Best reviewed commit by commit.
### Why?
External development tools need browser runtime-error state, not just
build errors, to display application failures. Reporting remains opt-in
so applications that do not need it avoid client serialization, HMR
transport, server formatting, buffering, and rebroadcast overhead.
### How?
Add `experimental.exposeRuntimeErrorsToHMR` for App Router development
with Webpack and Turbopack. Internal integrations can also enable the
same behavior without changing `next.config` by setting
`__NEXT_EXPOSE_RUNTIME_ERRORS_TO_HMR` to any non-empty value. When
enabled, the HMR WebSocket emits `runtimeErrors` snapshots containing:
- The current pathname and browser client/document identifiers.
- Error types, names, messages, and source-mapped stacks.
- Fatality and optional catching-boundary details (`default-global`,
`custom-global`, or `custom`).
Snapshots update as errors and navigation change. New or reconnected
observers receive the current state, and disconnecting a browser clears
its reported errors. An empty snapshot means there are no currently
reported errors; it does not guarantee application recovery.
Reporting is disabled by default. Pages Router, MCP `get_errors`, and
production behavior are unchanged.
<!-- NEXT_JS_LLM -->
Co-authored-by: Marcos Hernanz
<96699542+marcoshernanz@users.noreply.github.com>
In a previous PR, `mozjpeg` was set to true which might be the reason
output jpeg spikes cpu.
- https://github.com/vercel/next.js/pull/65846
I made a benchmark and found that disabling MozJPEG saved 72% CPU, with
average outputs 31% larger.
- https://github.com/lovell/sharp/issues/4603
So this PR adds a new experimental flag to enable (or rather disable)
mozjpeg and try with real workloads. This is useful becaues in many
cases, cpu is more costly than bandwidth since most CDNs provide [near
unlimited bandwidth](https://vercel.com/blog/introducing-flat-rate-cdn).
## Summary
Skip active-bundler config validation when `next/jest` loads
`next.config.*` under `PHASE_TEST`. Jest uses the Next.js SWC
transformer without running Turbopack or webpack, so bundler-specific
options should remain available without requiring phase-dependent user
config.
Add regression coverage for `experimental.turbopackRustReactCompiler`
and `experimental.cssChunking: 'graph'` in the test phase.
## Verification
- `pnpm test-unit test/unit/isolated/config.test.ts -t "Should allow
bundler-specific options during the test phase"`
<!-- NEXT_JS_LLM -->
## Summary
- add static gate conditions for the deployment CI variants used by
existing test suites
- support `@force-gate` on parameterized `describe.each` suites
- document and test the force-gate infrastructure needed by the
migration
This establishes the gate support used by the deployment-exclusion
migration PRs above it.
## Verification
- `pnpm typescript`
- `pnpm jest test/unit/gate/pragma-transform.test.ts
test/unit/gate/runtime.test.ts --runInBand`
<!-- NEXT_JS_LLM -->
A `describe` gated by a lazy `// @force-gate` (a condition read off the
fixture's resolved config, like `cacheComponents`) cannot be skipped at
collection time, so the gate runtime force-passes its tests at runtime
and `nextTestSetup` skips the fixture build. Hooks registered inside
such a `describe` still ran, because only the `it`/`test` globals were
wrapped. A hook that touches the `next` instance (for example a
`beforeEach` that reads `next.cliOutput`) then failed with `next
instance is not initialized yet` and failed the supposedly skipped
suite.
The gate runtime now wraps the
`beforeAll`/`afterAll`/`beforeEach`/`afterEach` globals as well and
skips a hook registered under a lazy `@force-gate` when that gate
evaluates false against the fixture's resolved config. Only a false lazy
force-gate skips a hook: an inverted `@gate` still runs its tests and
therefore needs its hooks, and a static `@force-gate` already skips the
whole `describe` at collection time. The hooks `nextTestSetup` registers
itself are marked with the new `ungatedHook` helper so that the skip
decision and the fixture cleanup still run.
The `cacheComponents disabled, edge app router` describe in
`test/e2e/cache-handlers-upstream-wiring` is converted from the
`process.env.__NEXT_CACHE_COMPONENTS` `describe.skip` ternary to `//
@force-gate !cacheComponents`, replacing a fake-green skip and
exercising the fixed path. The `!deploy` conversion of the remaining
describes is left to the branch that introduces the `deploy` condition.
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
This PR removes some unused code from the image optimizer.
- Remove redundant MIME validation
- Remove unreachable fallback error
These used to be necessary until PR
https://github.com/vercel/next.js/pull/82118 removed the fallback.
Before 82118:
```js
upstreamType = detectContentType(upstreamBuffer) || imageUpstream.contentType?.toLowerCase().trim()
```
After 82118:
```js
upstreamType = detectContentType(upstreamBuffer)
```
Now there is never a case where upstreamType is invalid since its
already been validated, there is no more fallback.
This PR is strictly a refactor, no logic should change.
The idea is that we eventually want to run the image optimizer transform
step in a child process. This PR refactors the code to make a new
`transform.ts` that is lightweight and should reduce some of the
overhead of loading a large module graph when spawning a new process.
## Summary
Builds on #97591. Webpack currently removes most of `.next` on devserver
startup. We can remove more as well.
This keeps the age-based removal introduced in #97591 and expands it to
`traces`, logs, etc. However, for any other directory that is not
`cache` or `lock`, completely remove it on startup. This includes types,
etc.
Apply age-based deletion to Turbopack-owned output that can be restored
from cache or recomputed, including route entries, chunks, assets,
service workers, middleware, and instrumentation. Completely remove
other dev output so logs, traces, generated types, diagnostics, and
future framework-owned output cannot be left as mixed generations.
Use exact-path exclusion sets in `recursiveDeleteSyncWithAsyncRetries`.
The complete pass retains `cache`, `lock`, and Turbopack-owned paths;
the age-based pass then sweeps those Turbopack paths. Both passes finish
before project creation.
## Verification
- `pnpm test-unit test/unit/recursive-delete.test.ts`
- `pnpm --filter=next types`
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
- Fixes https://github.com/vercel/next.js/issues/82357
- Closes https://github.com/vercel/next.js/pull/96985
### What?
`fetchInternalImage()` only rejects the internal response when
`statusCode` is falsy, and `MockedResponse` defaults it to `200`, so
nothing really gets rejected there. A `307` coming from a `redirects()`
entry in `next.config`, or a `404` for a path that isn't there, both
count as a successful image fetch.
That body then reaches `detectContentType()`, which returns `null`, and
all you get is:
```
The requested resource isn't a valid image for /path/to/image.png received null
```
Nothing in that line points back at the response that actually came in,
so a redirect looks exactly like a corrupt image.
### Why?
`fetchExternalImage()` right above already covers this with `if
(!res.ok)` and logs `res.status`. The internal path is the only fetch in
the file without that check, so the same failure gets reported at the
fetch for remote images, and three steps later as "not a valid image"
for local ones.
### How?
The guard now rejects anything outside the 2xx range, which is what
`res.ok` means, and the log line carries the status next to the href.
The thrown `ImageError` message stays generic, same as the external
path, so nothing new shows up in the HTTP response body.
Because that throw passes `mocked.res.statusCode` into `ImageError`, a
non-2xx internal response now reports its own status instead of always
landing on `400`. `ImageError` still maps anything under 400 to `500`,
so a redirect answers `500` and a `404` stays `404`. That is what
`fetchExternalImage()` has always done, and it lines the two up: a
missing local image now returns `404` just like a missing remote one.
Three assertions in `test/e2e/image-optimizer/util.ts` shift because of
that:
- `should not forward cookie header`: `/api/conditional-cookie` answers
`401` when the cookie is missing, so `400` becomes `401`
- `should error if the image file does not exist`: `/does_not_exist.jpg`
returns `404` now, with the internal response message
- `should error if the resource isn't a valid image`: this one requested
`/test.txt`, but the fixture in `public/` is `text.txt`, so it was
quietly exercising a missing file instead of a non-image file. Pointed
at the file that exists, it keeps its original `400` and finally tests
what its name says
Two unit tests cover the new guard, one `307` and one `404`. Every other
test in that file sets `statusCode = 200`, so they are untouched.
#96985 is open against the same report with a different angle: it
threads the status through `ImageUpstream` so the existing "isn't a
valid image" log can print it. This one stops the request at the fetch,
so the redirect never reaches the optimizer at all. Whichever one fits
better, feel free to close mine.
### Verification
- `jest test/unit/image-optimizer/`: 139/139, and the two new tests fail
against a build without the guard
- `pnpm test-dev
test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97
- `pnpm test-start
test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97
- `prettier --check` clean on the three changed files
<!-- NEXT_JS_LLM -->
Co-authored-by: Steven <steven@ceriously.com>
Reverts #97875 and bumps `sharp` from `^0.35.3` to `^0.35.4`,
re-enabling AVIF image optimization.
`minimumReleaseAge` interplay (verified with pnpm 10.33.0 and 11.22.0 in
a standalone repo with a `file:` dependency declaring sharp as an
optional dependency):
- sharp 0.35.4 was published on 2026-08-26 and is younger than this
repo's 48h `minimumReleaseAge`.
- pnpm aborts the whole install with
`ERR_PNPM_NO_MATURE_MATCHING_VERSION` when no in-range release satisfies
the age gate. This applies to `optionalDependencies` (no warn-and-skip)
and through `file:` dependencies alike.
- The gate also covers the `@img/sharp-*` binary packages sharp 0.35.4
depends on, so excluding only `sharp@0.35.4` is not enough.
- `pnpm-workspace.yaml` therefore exempts `sharp@0.35.4` and
`@img/sharp-*` (pnpm does not allow version qualifiers on name patterns
in this setting). Both entries can be dropped once 0.35.4 is older than
48 hours.
- Downstream users who configure their own `minimumReleaseAge` will hit
the same hard install failure when installing a release with this bump
until 0.35.4 ages out, unless they add the same excludes. Keeping
`^0.35.3` is not a safer alternative: age-gated installs would then
silently resolve the vulnerable 0.35.3.
Part of https://linear.app/vercel/issue/VOC-34654/
---------
Co-authored-by: Steven <steven@ceriously.com>
The test suite has accumulated a bunch of patterns for disabling tests
that are known to fail under some configuration: `it.skip`, `if
(isNextDev) { test('skipped in dev mode', () => {}); return }`, whole
describes toggled off by checking `process.env.__NEXT_CACHE_COMPONENTS`.
These all have the same flaw: nothing tells you when the thing you
skipped starts working. The test stays disabled forever, and the
workaround it was guarding rots along with it.
React solves this with the `@gate` pragma, and this PR ports it to the
Next.js e2e harness:
```ts
// Blocked on the optimization that marks a route as fully static when
// no dynamic params are referenced in Server Components.
// @gate !cacheComponents
it('navigates to a page with a lazily-generated static param', async () => {
// body unchanged
})
```
The test still runs. If the condition is false and the test fails, the
failure is absorbed and the suite stays green. If it _passes_, the suite
fails: the gate is stale, delete it. So instead of a skip that hides a
fixed bug indefinitely, you get a CI failure the day the fix lands.
When the condition is static, the inversion is Jest's own `test.failing`
under the hood. A lazy condition isn't known until the fixture's
resolved config is read inside the body, so those tests invert at
runtime instead.
`// @force-gate <condition>` skips for real — for tests that can't even
be attempted (prefetching is disabled in dev, deploy has no local build
output, the fixture won't build under the condition), and for tests of a
new API, where the disabled state can only throw and running it proves
nothing:
```ts
// Prefetching is disabled in dev, so this suite has nothing to test.
// @force-gate prefetching
describe('segment cache prefetch scheduling', () => {
// ...
})
```
There's no staleness check in that case, so this is a judgment call:
prefer `@gate` when the off state fails for a meaningful reason — the
flag changes behavior that already exists — and `@force-gate` when the
body can only throw because the API doesn't exist. A static condition
(mode, bundler) resolves at collection time into a normal Jest skip. A
lazy condition resolves at runtime, and when a lazy force-gate on a
describe is false, we skip the fixture build entirely — that's what
makes it usable for suites whose fixtures are build-incompatible with
the condition. (One caveat: Jest has no way to skip a test that's
already running, so these report as passing with a warning in the log,
not as skipped.)
Conditions live in a hand-written registry. I considered deriving the
lazy ones from the config schema automatically, but a gate is a claim
about which dimension of the test matrix explains a failure, and I'd
rather each of those claims be spelled out with a description.
Referencing an undeclared name fails the suite at collection time, so a
typo can't silently disable a gate.
The important design decision for lazy conditions is that they read the
fixture's _resolved_ config, never `process.env`. The env var isn't the
truth: `__NEXT_CACHE_COMPONENTS=true` only applies when the fixture
doesn't set `cacheComponents` itself, and config resolution implies
flags the fixture never mentions (`cacheComponents: true` alone turns on
`experimental.ppr`). Resolution happens in a child process, because
in-process `loadConfig` would leak the fixture's `.env` files into the
Jest worker. Suites with no lazy gate never pay for any of this.
The condition expression is parsed using a small grammar (also ported
from the React repo). An expression that doesn't parse fails the suite:
```ts
// @gate mode === 'start' && !cacheComponents
// @gate !(turbopack || rspack)
```
There's also a runtime version, mirroring React's `gate(flags =>
flags.enableFoo)`, for tests that run under both states but assert
differently (and for `it.each`, where the pragma can't attach):
```ts
import { gate } from 'next-test-utils'
it('renders the fallback', async () => {
if (await gate((conditions) => conditions.cacheComponents)) {
// PPR: the fallback is part of the static shell
} else {
// fully dynamic: the fallback streams in
}
})
```
It also accepts the pragma expression language as a string: `await
gate('cacheComponents && !dev')`.
Docs are in `test/lib/gate/README.md`; `test/unit/gate/` covers the
transform, the expression language, and the runtime.
## Summary
Turbopack dev never removes the content-hashed chunks it supersedes, so
`.next/dev` grows without bound. Webpack dev avoids this by wiping
`distDir` on every start.
Adds a `maxAgeMs` argument to `recursiveDeleteSyncWithAsyncRetries`:
delete files whose mtime is at least that old, remove directories once
empty. Turbopack dev passes one week.
## Verification
- `pnpm test-unit test/unit/recursive-delete.test.ts`
- ~Not run:~ dev-server behavior against a real `.next/dev`
<!-- NEXT_JS_LLM -->
Store/load the encryption keys at `.next/server/preview-props.json`
(which are always needed) as opposed to including the giant
`.next/prerender-manifest.json`
The manifest is added to `required-server-file.json#files` so it keeps
getting included in prod
`.next/prerender-manifest.json` is unchanged for backwards compatiblity
reasons
This is work towards the goal of not including the one giant
prerender-manifest at runtime in the serverless function
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:
## For Contributors
We encourage you to use AI to assist you in researching, creating, and
reviewing changes. However, you must review and deeply understand the
contributions you are making. For this reason, **pull request
descriptions from external contributors must be written by a human**.
### Improving Documentation
- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide
### Fixing a bug
- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md
### Adding a feature
- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md
### Signed commits
- This repository requires verified commit signatures on protected
branches.
- If this pull request is blocked for unsigned commits, re-sign the
commits and force-push the branch.
- A `Signed-off-by` line in the commit message is not enough.
## For Maintainers
- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Closes NEXT-
Fixes #
-->
## Summary
Enable the project-local TypeScript CLI checker by default during `next
build`, while preserving `experimental.useTypeScriptCli: false` as an
opt-out to the TypeScript compiler API. Update the TypeScript 7
guidance, diagnostics, tests, and documentation to match the new
default.
## Verification
- `pnpm exec jest --runTestsByPath test/unit/isolated/config.test.ts`
- Pre-commit lint-staged checks passed (Prettier and ESLint)
- Not run: `pnpm test-start-turbo
test/production/app-dir/typescript-cli/typescript-cli.test.ts` (isolated
fixture dependency installation was blocked by unavailable npm registry
access)
- Not run: `pnpm --filter=next types` (repository has pre-existing
unrelated TypeScript errors)
<!-- NEXT_JS_LLM -->
How long do you expect this to wait at worst (before failing)?
```js
await retry(async () => {
expect(await browser.elementById('never-appears').text()).toBe('x')
}, 15_000)
```
After this PR, the answer is 15 seconds. (Well, technically, 15 seconds
+ whatever last attempt is, so 20.)
But before this PR, the answer was 3 minutes.
## What?
There's a bug (IMO) in `retry`. Instead of waiting for the time you give
it, it counts tries.
For example, `retry(fn, 15_000)` reads as "keep trying for 15 seconds".
However, what it actually was doing is divide 15000 by the 500ms
interval and try 30 times, sleeping 500ms in between every try.
This is a problem because **it wasn't measuring the stall time of each
try itself.**
When a try is cheap, it doesn't matter. But a try that looks for an
element in the browser isn't cheap: if the element isn't there, the
selector waits 5 seconds before giving up.
In the example above, each try sits in the selector for its full 5
seconds, and there are 31 of these intervals, plus the 15 seconds of
sleeping in between. This ends up waiting 170103ms, i.e. three minutes
for a wait that says 15 seconds.
Jest kills the test at its own timeout long before that, and the actual
error gets thrown away. What's left is `Exceeded timeout of 120000 ms`,
which doesn't say which assertion never came true. In the one I measured
it was `page.waitForSelector: Timeout 5000ms exceeded`. My agent lost an
hour trying to make sense of this twice now.
## Fix
We count time now. After the fix, the retry watches the clock and stops
when its time is up. It still always makes at least one try. The
interval no longer has to divide the duration, because nothing counts
steps any more. If the last try is slow, we're gonna wait for that last
try to end (so the timeout is still not *strictly* respected) so that we
can show the error in the correct test.
Cheap waits behave exactly as before. In my local run, a try that had to
repeat cost 251ms at the median, 459ms at the worst. So the only calls
this changes are ones that were already failing, and those now fail with
their own error instead of a test timeout. One flaky dev test that used
to die at 120 seconds with nothing to show now fails in 19.5 seconds
saying `Failed to retry within 15000ms (waited 16022ms)`, next to nine
tests that take 2.2 seconds each.
I added some regression tests. Since it's a test helper, a unit test
seemed fine.
## What this means for existing tests
The catch is that a call whose tries are slow now gets less time than it
used to, and this helper has 2110 call sites, so CI is the real check.
It found one file: the hash scroll tests in `navigation.test.ts` wait
with the default 3 seconds, and each try there costs 150-300ms on a
runner, so they were really getting about 5. They need more than 3, so
they failed on two dev shards. I gave them 10.
If this is green, we can merge it in isolation from the rest of the
stack.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
We were missing some test coverage for image optimization. This PR adds
tests for many missing image formats.
Additionally, this PR also ensures the sharp encoders match that
allowlist from detectContentType() such that future versions of sharp
that enable new encoders by default are not automatically enabled in
next/image.
Practically, there is no change to the end user because
detectContentType() runs before sharp so that is the true allowlist, but
this gives us a little more control to align the sharp allowlist as well
(since sharp can release new features in semver minor).
## Summary
Adds `experimental.devMemoryThresholdRestart`, defaulting to `true`, so
projects can set it to `false` to opt out of the development server
restart that occurs when V8 heap usage exceeds 80%. The option has no
effect on `next start`.
## Verification
- Focused config-loading and memory-threshold unit suites passed
- `pnpm --filter=next build` completed JavaScript compilation, then
failed on pre-existing `sharp` and `@vercel/detect-agent` declaration
errors unrelated to this change
<!-- NEXT_JS_LLM -->
- `detectContentType` test was too aggressive about asserting completion
in 1ms, let's give it several tries.
- We weren't correctly setting up pnpm overrides in tests, which breaks
things whenever canary comes out.
### What?
Fire-and-forget `setTimeout` calls in the Edge runtime sandbox (e.g.
from middleware) are tracked forever by Next.js's sandbox
`TimeoutsManager`, even after the timeout has run. In a long-lived
standalone/self-hosted server this makes tracked timeout resources grow
monotonically, producing step-like heap growth and eventual OOM.
### Why?
`TimeoutsManager.add()` pushes each timeout id into a `resources` array.
When a one-shot timeout fires naturally, `webSetTimeoutPolyfill`'s
`finally` clears the underlying Node timer (the workaround for
nodejs/node#53335) but never removes the id from the manager. The id is
dropped only if user code calls `clearTimeout(id)` or the whole module
context is torn down (`clearModuleContext`, introduced in #57235). The
natural-completion path — module context stays alive, timeout finishes
on its own — was missing, so ids accumulate for the lifetime of the
process.
### How?
- `TimeoutsManager.create()` now wraps the callback so the timeout
untracks its own id once it has run. The Node-timer workaround and the
`this`/args binding in `webSetTimeoutPolyfill` are preserved.
- Refactored the base `ResourceManager.remove()` into `untrack()` (stop
tracking) + `destroy()`, so natural completion can release tracking
without a redundant `clearTimeout`.
- Intervals are intentionally unchanged: they fire repeatedly and must
remain tracked until `clearInterval`/`removeAll`.
- Added a unit test covering natural release, no monotonic growth across
repeated rounds, the `clearTimeout`/`remove` path, callback `this`/args
binding, and that intervals stay tracked.
Verified: `pnpm --filter=next types` clean, prettier/eslint clean, and
the new unit test fails on the current behavior (reverting only the
release logic) and passes with the fix.
Fixes#95094
---
**Update:** this branch now lands @berry95's commit from #94794 first
(`Set`-based resource tracking, which also makes `untrack`/`remove`
O(1)), with the fix rebased on top.
---------
Co-authored-by: Berry Kim <wlsdud40@gmail.com>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Recreation of https://github.com/vercel/next.js/pull/95370
### What's the problem?
A `POST` (or `PUT`/`PATCH`) request that passes through middleware
returning `NextResponse.next()` hangs indefinitely when the downstream
handler reads the body via Node's `Readable.toWeb()`. The request never
completes and eventually times out.
Reproduction: https://github.com/abir-taheer/next-js-readable-stream-bug
### Root cause
When middleware runs, `runMiddleware` clones the request body and later
calls `finalize()`, which grafts the buffered stream back onto the
original `IncomingMessage` via `replaceRequestBody()`
(`packages/next/src/server/body-streams.ts`). `replaceRequestBody`
copies the buffered stream's enumerable properties onto the request.
The buffered stream (`p2`) was a `PassThrough` — a `Duplex` — so its
writable-side internals (`_writableState` plus the enumerable `Writable`
methods like `write`/`end`) were copied onto the `IncomingMessage`.
Because that `_writableState.finished` is `false`, Node stream utilities
that inspect it — including `Readable.toWeb()`, which uses
`finished()`/end-of-stream detection — treat the request as a still-open
writable stream and wait forever.
`NextResponse.rewrite()` is unaffected (it builds a new internal request
and skips this path), and `GET`/`HEAD` requests are fine because there
is no body to clone.
### The fix
`p2` is only ever fed with `.push()`, so it never needs a writable side.
Making it a plain `Readable` instead of a `PassThrough` keeps the
finalized request a pure `Readable`, so `Readable.toWeb()` (and any
other duck-typing based on `_writableState`) behaves correctly. No
behavior change for the existing consumers, which only read the stream.
### Testing
Added `test/unit/body-streams.test.ts`, which drives the real clone →
`finalize()` flow and asserts the finalized request:
- is no longer writable (`_writableState` is `undefined`), and
- is fully consumable via `Readable.toWeb()` (this hangs before the
fix).
Fixes#95335
<!-- NEXT_JS_LLM_PR -->
Co-authored-by: UditDewan <udit.dewan21@gmail.com>
Co-authored-by: Baradhan-Madhu <26barum@gmail.com>
## Summary
- add `experimental.useTypeScriptCli` so projects can explicitly run
their local `tsc` during `next build`, including TypeScript 7 while the
legacy JavaScript API is unavailable
- preserve the TypeScript API backend as the default, with TypeScript
6-compatible dependency installation and actionable TypeScript 7
migration guidance
- load effective compiler metadata through `tsc --showConfig`, preserve
inherited path origins, stream native diagnostics, redirect incremental
state, and forward worker termination to the compiler process tree
- document the experimental behavior and cover TypeScript 6/7, both
bundlers, raw diagnostics, full-project checking, dependency selection,
and cleanup
## Demo
```
pnpm build
> cna@0.1.0 build /Users/timneutkens/projects/sandbox/cna
> next build
▲ Next.js 16.3.0-canary.81 (Turbopack)
✓ Running next.config.ts took 23ms
- Cache Components enabled
- Experiments (use with caution):
✓ useTypeScriptCli
Creating an optimized production build ...
✓ Compiled successfully in 3.9s
Running TypeScript ...
app/page.tsx:8:3 - error TS2322: Type 'string' is not assignable to type 'number'.
8 return count++ + "abc";
~~~~~~
Found 1 error in app/page.tsx:8
Failed to type check.
Next.js build worker exited with code: 1 and signal: null
ELIFECYCLE Command failed with exit code 1.
```
## Verification
- `pnpm test-start-turbo
test/production/app-dir/typescript-cli/typescript-cli.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/typescript-cli/typescript-cli.test.ts`
- `pnpm test-start-turbo
test/production/ci-missing-typescript-deps/index.test.ts`
- `pnpm test-dev-turbo
test/development/typescript-native-preview/index.test.ts`
- `pnpm test-start-turbo test/production/typescript-basic/index.test.ts`
<!-- NEXT_JS_LLM_PR -->
Adds a global config option called `partialPrefetching` that changes the
behavior of `<Link prefetch={true}>`.
When Partial Prefetching is enabled, only Cache Components are included
in a prefetch response. Dynamic data is omitted. This is the same idea
as Partial Prerendering for initial page loads, now applied to client
navigations.
In the initial Cache Components release, Partial Prefetching was enabled
for any Link that did not explicitly set a `prefetch` prop. However, if
the `prefetch` prop was set to `true`, then the Link retained the old
behavior where the _entire_ page was prefetched, including dynamic data.
This was to ease the migration path for existing apps that already
relied on dynamic data being included in the prefetch.
For new apps that have Cache Components enabled from the start, it's
suggested to also enable `partialPrefetching`.
This PR also adds a per-segment opt-in: a route segment can set
`unstable_prefetch = 'partial'` to enable Partial Prefetching just for
that segment, so apps can migrate gradually before enabling it globally.
<!-- NEXT_JS_LLM_PR -->
### What?
Flips the default `experimental.instantInsights.validationLevel` from
`'manual-warning'` to `'warning'` so Cache Components apps get
instant-navigation validation across all pages by default.
### Why?
`'manual-warning'` only validates pages that explicitly export
`unstable_instant`, so apps see nothing unless they opt in. `'warning'`
is what users have been turning on manually to actually see the feature
(`v0`, `vercel-site`).
### Test coverage
- Unit: `instant-config-normalization.test.ts` pins the framework
default at `'warning'`.
- Integration: new `instant-validation-level-default/` fixture (no
`instantInsights` in config) asserts implicit dev validation fires, and
that build is unaffected.
### Collateral test fixtures
Tests whose intent is unrelated to instant validation
(router-autoscroll, owner-stack, hmr-iframe, next-image,
server-source-maps, etc.) now hit new redboxes/console warnings because
their fixtures incidentally use dynamic data. Each opts out with
`experimental: { instantInsights: { validationLevel: 'manual-warning' }
}` in `next.config`.
### Open question
With no `'off'` / `'info'` tier today, silencing Insights after this
change requires setting `validationLevel: 'manual-warning'`. Is that
acceptable? Should we inform of this anywhere?
<!-- NEXT_JS_LLM_PR -->
### What?
Defaults `experimental.useNodeStreams` to `true` while keeping the
experimental config flag and explicit `false` opt-out available. Removes
redundant dedicated node-stream CI jobs because the ordinary path
exercises Node streams after this change.
Depends on the standalone test compatibility update in #94347, which
intentionally remains outside the Node streams PR stack.
### Why?
Node streams can be exercised through the standard Node.js App Router
test path once they are the default, allowing a smaller rollout before
the larger follow-up that removes the flag and obsolete plumbing.
CI exposed one necessary compatibility fix for this intermediate state:
projects with a `next.config.*` received the new default after raw
experimental-feature processing, leaving the runtime stream selector on
the web implementation while compiled app code selected Node stream
helpers.
### How?
- Set `defaultConfig.experimental.useNodeStreams` to `true`.
- Synchronize `__NEXT_USE_NODE_STREAMS` from the fully resolved config,
including cached and standalone resolved configs, so defaults and
adapter modifications are reflected at runtime.
- Preserve the current public config/schema and explicit `experimental:
{ useNodeStreams: false }` opt-out; edge bundles continue to define the
stream flag as `false`.
- Remove the dedicated node-stream test jobs and now-unused manifest;
the affected streaming assertion compatibility changes are isolated in
#94347.
### Verification
Run before extracting the prerequisite assertion changes into #94347:
- `pnpm --filter=next types`
- `pnpm --filter=next build`
- `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm
test-start-webpack
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts`
- `IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1
__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-turbo
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts`
- `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm
test-start-webpack
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
- `pnpm test-dev-webpack
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
- `pnpm test-dev-turbo
test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts
test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/use-node-streams-env-precedence/use-node-streams-env-precedence.test.ts`
- `pnpm test-start-webpack test/e2e/app-dir/app/standalone.test.ts`
<!-- NEXT_JS_LLM_PR -->
rootParams is now available by default. the flag is removed.
There are still intentional limitations. For instance rootParams cannot
be used in route handlers and Server Actions. The feature will be
expanded with some support for this in the future.
### What?
Converts every test under `test/integration/` to an isolated test
running through `nextTestSetup` (under `test/e2e/`, `test/production/`,
`test/development/`, or `test/unit/`), then deletes `test/integration/`
along with the legacy CI orchestration that was specific to it.
- `test/integration/` removed entirely (~327 test suites)
- New isolated suites added across the existing folders:
- `test/e2e/` — 175
- `test/production/` — 130
- `test/development/` — 43
- `test/unit/` — 1
- `.github/workflows/build_and_test.yml` and `run-tests.js` no longer
have any `integration` branches
- `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a
small number of tests that drive their own proxy/static-export server
can keep using `next.browser(...)` instead of importing `next-webdriver`
directly
### Why?
`test/integration/` predated `nextTestSetup` and ran tests directly
against the source checkout via custom helpers (`launchApp`,
`nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`,
…). Each suite hand-rolled its own dev/start/build orchestration,
fixture mutation, and process management.
The isolated test model used by the rest of the repo gives each suite an
isolated working directory containing a packed `next.tgz` install, a
uniform `next.start()` / `next.build()` / `next.fetch()` /
`next.browser()` API, and the same lifecycle for dev, start, and deploy
modes — so a single set of assertions covers all three. Deploy-mode
skips and per-feature gates are expressed declaratively
(`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`)
instead of branching on `process.env`.
Removing `test/integration/` lets us:
- Delete the bespoke orchestration code in the CI workflow and
`run-tests.js`
- Run every converted suite consistently in dev, start, and deploy modes
(where applicable)
- Reproduce every test locally with the same `pnpm
test-{dev,start}-{turbo,webpack}` commands; no separate `integration`
path
- Open the door to running `test/production` against deployments in the
future (the converted suites already declare `skipDeployment` so they
can be flipped on)
### How?
Mechanical conversion per suite, with targeted clean-ups:
1. **Per-suite conversion.** Each
`test/integration/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.test.ts` under the right folder based on what the
original exercised:
- `launchApp` / dev-only assertions → `test/development/`
- `nextBuild` + `nextStart` / start-only assertions → `test/production/`
- Both → `test/e2e/`
- The one pure jsdom render check (`link-without-router`) → `test/unit/`
2. **API mapping.** Custom helpers were replaced by `nextTestSetup`
equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`,
`runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`,
`webdriver(...)` → `next.browser(...)`. Fixture mutations switched from
raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg
`runWithTempContent` callback when the change has a defined scope) and
`next.deleteFile`.
3. **Deploy-mode handling.** Suites that can't run in deploy mode (use
`patchFile` / `next.build()` / depend on local CLI output) declare
`skipDeployment: true` and early-return on the `skipped` boolean. Suites
where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare
`disableAutoSkewProtection: true`.
4. **`next.browser({ baseUrl })`.** A handful of tests
(`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both
`react-virtualized` suites) need to drive a separate server (a
static-export server or an `http-proxy` instance) rather than the
Next.js process. Instead of importing `next-webdriver` directly, those
tests now pass `{ baseUrl: <port|url> }` to `next.browser()`. For the
proxy cases, the proxy was moved into `server.js` inside the fixture and
`http-proxy` declared via the `dependencies` option of `nextTestSetup`,
so the test runs with a fully isolated dependency graph.
5. **CI clean-up.** With `test/integration` gone, the `test
integration*` jobs and `integration-tests-manifest`-related logic in
`.github/workflows/build_and_test.yml` were removed, and `run-tests.js`
no longer has the `integration` test-folder branch.
6. **Validation.** The PR was iterated against multiple full CI runs;
the remaining failures on the latest run are pre-existing flakes
(segment-cache 60s `act` timeouts in turbopack-prod) or transient
infrastructure issues unrelated to the conversion.
Adds `experimental.instantInsights.validationLevel` so an app can opt
into instant validation for all routes by default.
There are two public levels
* `manual-warning`: currently the default. this mode won't enable
instant validation except where you add `export const unstable_instant =
true` into your Layouts, Pages, and Default files
* `warning`: This mode will enable instant validation for all Page and
Default files. You can override this with `export const unstable_instant
= false` in Layouts, Pages, and Default files to tune what navigations
are validated.
In addition this change disables build time validation.
There is a now undocumented way to get back to running build validation
with
* `experimental-error`
* `experimental-manual-error`
The build validation isn't ready for users because we need to overhaul
the setup where you provide cookies and other sample values so we can
run validation without a live session like we do in dev.
To further support this the two remaining object properties that are
also not ready for active use, `samples` and `from` are being updated to
`unstable_samples` and `unstable_from` respectively.
We will continue to iterate on build time validation but may remove
these APIs or alter their semantics as we figure out a more ergonomic
way to support this setup.
In anticipation of multiple levels the object form of `instant` can be
provided a level value `warning` or `experimental-error`. this latter
option is similarly undocumented so documented behavior will only cover
`warning` which is generally a synonym of true unless you are activating
error by default
## For Contributors
### Adding a feature
Adding a new ESLint rule to `eslint-plugin-next` reporting
`location.href = [relative URL]` or `location.assign([relative URL])`
usage.
For some reason, some LLM models prefer this incorrect pattern, and
avoid `redirect()` or `useRouter().push()` at all costs. But at least
many of them do respect ESLint, by adding a new rule we can enforce
correct behavior.
The error documentation and unit test cases are also added.
---------
Co-authored-by: Joseph <sephxd1234@gmail.com>
Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
In a previous PR https://github.com/vercel/next.js/pull/88183, we added
`images.maximumResponseBody` but in only applied to external images.
This PR ensures the same config also applies to internal images.