### What?
Adds `experimental.serverFastRefresh` to `next.config.js` so users (and
Next.js plugins) can opt out of server-side Fast Refresh without needing
to pass a CLI flag.
Also adds a `--server-fast-refresh` positive CLI flag (hidden) alongside
the existing `--no-server-fast-refresh`, and emits a warning when the
CLI flag and config value conflict. This is a necessary implementation
detail for `commander` to detect when no option is provided.
### Why?
Certain Next.js plugins have encountered issues with server Fast Refresh
and need a way to disable it programmatically via config rather than
requiring users to modify their start scripts. The CLI-only opt-out
(`--no-server-fast-refresh`) was insufficient for this use case.
### How?
- **`config-shared.ts` / `config-schema.ts`**: Added
`experimental.serverFastRefresh?: boolean` to the `ExperimentalConfig`
type and Zod schema.
- **`router-server.ts`**: Resolves the effective `serverFastRefresh`
value at startup by combining the CLI flag (`opts.serverFastRefresh`)
and `nextConfig.experimental.serverFastRefresh`, with CLI taking
precedence. Emits a `Log.warn` when both are set to conflicting values.
Defaults to `true` when neither is specified.
- **`bin/next.ts`**: Fixed the Commander option definition for
`--no-server-fast-refresh`. Commander's `--no-X` pattern implicitly
creates a positive `--server-fast-refresh` option defaulting to `true`,
which meant `opts.serverFastRefresh` was never `undefined` and the
config value could never take effect. Fixed by explicitly registering a
hidden `--server-fast-refresh` option with `.default(undefined)`.
- **Tests**: Added two new test suites in
`test/development/app-dir/server-hmr/server-hmr.test.ts`:
- `server-hmr config opt-out` — verifies that setting
`experimental.serverFastRefresh: false` in config causes unmodified
dependencies to be re-evaluated (i.e., server HMR is disabled).
- `server-hmr CLI/config conflict warning` — verifies that passing
`--no-server-fast-refresh` when config has `serverFastRefresh: true`
logs the conflict warning.
### Test Plan
- [x] Added e2e development tests
(`test/development/app-dir/server-hmr/server-hmr.test.ts`) — all 8 tests
pass
---------
Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
### What?
Metadata routes (`manifest.ts`, `robots.ts`, `sitemap.ts`, `icon.tsx`,
`apple-icon.tsx`, etc.) were not being hot-reloaded in Turbopack dev
mode — changes to those files would not be reflected on subsequent
requests until a full server restart.
### Why?
PR #91466 extended `usesServerHmr = true` in `clearRequireCache()` (in
`hot-reloader-turbopack.ts`) from `app-page` entries only to **all**
`app`-type entries (pages + route handlers). The motivation was correct:
regular route handlers like `app/api/hello/route.ts` use Turbopack's
in-place module update model and benefit from server HMR.
However, metadata routes (`/manifest.webmanifest/route`,
`/robots.txt/route`, etc.) are also `app`-type entries but they are
**not** suitable for in-place server HMR. When `usesServerHmr = true`
for a metadata route, `clearRequireCache()` skips two critical
invalidation steps:
1. Deleting the compiled chunk from `require.cache`
2. Calling `__next__clear_chunk_cache__()`
Without those steps, the old module stays in-memory and all subsequent
requests to `/manifest.webmanifest` (etc.) return the stale content.
### How?
Added an `!isMetadataRoute(entryPage)` guard to the `usesServerHmr`
expression in `clearRequireCache()`. This restores full cache
invalidation for metadata routes on every rebuild while leaving regular
route handler server HMR (added in #91466) intact.
```ts
// Before
const usesServerHmr =
serverFastRefresh &&
entryType === 'app' &&
writtenEndpoint.type !== 'edge'
// After
const usesServerHmr =
serverFastRefresh &&
entryType === 'app' &&
writtenEndpoint.type !== 'edge' &&
!isMetadataRoute(entryPage) // ← metadata routes always clear the cache
```
`isMetadataRoute('/manifest.webmanifest/route')` → `true` (excluded from
server HMR)
`isMetadataRoute('/api/hello/route')` → `false` (keeps server HMR, no
regression)
Also added a regression test: `metadata route hmr > reflects manifest.ts
changes on fetch/refresh` in the `server-hmr` test suite, with a
`manifest.ts` fixture that starts at `name: 'Version 0'`. The test
patches the file and asserts the updated JSON is returned on the next
fetch.
Fixes#91981
---------
Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
TypeScript 6 treats `moduleResolution: "node"` as deprecated (`node10`)
and can fail builds with `TS5107`.
This update keeps Next.js tsconfig defaults compatible with modern
TypeScript by preferring `moduleResolution: "bundler"` for modern module
modes, while preserving `"node"` for `commonjs`/`amd` configs where
`bundler` is invalid.
Also updates the related tests/snapshots to match the new defaults.
Otherwise you get a `Dependency tracking is disabled` / infinite loop
when trying to use both turbopack.rules.loader and turbopackLoader at
the same time
## What
Rename the HMR WebSocket path from `/_next/webpack-hmr` to `/_next/hmr`.
## Why
The `webpack-hmr` name is a leftover from when webpack was the only
bundler. Now that Turbopack is the default bundler for both `next dev`
and `next build`, the path name is misleading. The generic `/_next/hmr`
better reflects that this endpoint is bundler-agnostic.
## Changes
- **Client source** (`page-bootstrap.ts`, `web-socket.ts`): updated the
WebSocket connection path
- **Server** (`router-server.ts`): updated the HMR request detection
path
- **Turbopack** (`turbopack-dev-server/src/lib.rs`): updated the
fallback WebSocket path check
- **Tests**: updated all test files referencing the old path
- **Docs** (`version-12.mdx`): added a note that the path was renamed to
`/_next/hmr` in Next.js 16, while keeping the original v12 examples
intact
## What?
Extracts the bundling infra to have the Node.js streams experimental
flag out of #89859
This only adds the bundle and the v8-jit skill. It does not use the
bundle yet, and it's not switching code base on the env var.
There will be follow-up PRs for those.
App router handlers are already built with the Turbopack runtime, so
they can use server HMR. **We also already get subscription events for
their chunks.** However, unlike app pages which use
`__next_app__.require()` for dynamic devModuleCache lookup, route
handlers capture userland exports statically in AppRouteRouteModule at
construction time. This change makes routes behave a lot like pages,
dynamically loading the user's code when requests are made. This way, we
can freely invalidate and reload it when changes are made without
evicting the entire require cache.
Only `next dev` uses this lazy evaluation, as `next start` continues to
eagerly import route handlers.
This change removes the `isAppPage` restriction from `usesServerHmr`,
extending server HMR coverage to all App Router entries (pages and route
handlers) built with the Node.js runtime.
For route handlers, we also clear the entry chunk from Node.js
`require.cache` on each rebuild so the next `requirePage()` call
re-executes the entry and obtains fresh module exports from
`devModuleCache` (which HMR updates in-place).
Test Plan: Added an additional e2e test, `with-dep`
This improves the blocked-request warning so it names the actual dev
resource being requested and gives clearer guidance on how to allow it.
When the source host is known, the message includes an inline
`allowedDevOrigins` config snippet; when the source is missing or
opaque, it explains why Next.js cannot infer a host to allow.
This removes the warn-only default behavior and enforces the dev-origin guard by default. Cross-origin requests to internal dev resources now block unless they match the built-in local allowlist or an explicit `allowedDevOrigins` entry. The tests are expanded to cover default blocking, configured-but-not-allowlisted hosts, missing Referer in the no-cors path, and same-site requests without an Origin, and the docs are updated to match the new behavior.
This PR makes configured `allowedDevOrigins` apply to cross-site no-cors dev asset requests. When browsers omit Origin for subresource loads, the dev guard now falls back to `Referer` so explicit allowlisted hosts can load `/_next/*` resources in development.
Previously, when `allowedDevOrigins` was configured, cross-site no-cors requests to internal Next.js dev resources were still blocked even for allowlisted hosts, because that code path never consulted the allowlist.
### What?
Removes the `experimental.devCacheControlNoCache` config option entirely
and hard-codes `no-cache, must-revalidate` as the dev server
`Cache-Control` header value.
Previously the option controlled whether the dev server responded with:
- `no-store, must-revalidate` (default, `false`)
- `no-cache, must-revalidate` (opt-in, `true`)
This PR first flips the default to `true`, then removes the option
altogether — making `no-cache, must-revalidate` unconditional in all dev
code paths.
### Why?
`no-cache` is strictly better than `no-store` for the dev server:
- `no-cache` allows the browser to revalidate (conditional
`If-None-Match`/`If-Modified-Since` requests), letting the server
respond with `304 Not Modified` when nothing changed → faster page loads
during development.
- `no-store` forces a full re-fetch every time, discarding valid cached
responses.
Since `no-cache` is the correct behavior for all dev users, the toggle
has no remaining value and can be removed to simplify the codebase.
### How?
**Two commits:**
1. `bcec825` — Flip the default from `false` → `true`; update
tests/fixtures/manifest to reflect the new default.
2. `e6e919f` — Remove the option entirely:
- Deleted `devCacheControlNoCache?: boolean` from `ExperimentalConfig`
interface, `config-schema.ts` Zod schema, `defaultConfig`,
`NextConfigRuntime`, and `getNextConfigRuntime()`.
- Replaced all four conditional ternaries in `base-server.ts`,
`router-server.ts`, `pages-handler.ts`, and `app-page.ts` with the
hard-coded string `'no-cache, must-revalidate'`.
- Deleted the `dev-cache-control-no-cache-disabled` test suite (was
testing the `false` path which no longer exists).
- Simplified the remaining `dev-cache-control-no-cache` test (removed
experimental framing).
- Synced `rspack-dev-tests-manifest.json`.
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
## What?
This PR improves CSS parsing error handling in Turbopack by making parse errors recoverable instead of fatal.
1. **Extracting CSS parsing logic** into a new `parse_css_stylesheet` function that handles both parsing and CSS module validation in one place
2. **Fixing error recovery** when CSS minification fails — instead of immediately returning an unparsable result, the code now re-parses the original CSS to recover valid styles
3. **Improving error reporting** by renaming variables for clarity (`source` → `issue_source`) and ensuring errors are properly emitted before attempting recovery
4. **Adding comprehensive tests** for CSS parse error scenarios with both snapshot tests and e2e tests
The snapshot test fixture exercises several CSS features to confirm they survive error recovery:
- Basic selectors (`.before`, `.after`) — valid rules are preserved when surrounded by invalid ones
- `::highlight(highlight-test)` — CSS Custom Highlight API pseudo-element
- `scroll-marker-group`, `::scroll-marker`, `::scroll-marker:target-current` — CSS scroll marker pseudo-elements (new spec)
- `oklch()` color values — converted to `#rrggbbaa` + `lab()` fallbacks for broader browser compatibility
## Why?
Previously, when CSS minification encountered an error, the entire stylesheet would be marked as unparsable, losing any valid CSS that could have been recovered. This meant `turbopackIgnoreIssue` could not suppress the errors since the module produced no output regardless.
Now, when lightningcss recovers from parse errors (collecting them as warnings), issues are still emitted but processing continues with the recovered stylesheet. Only truly fatal parse errors (where `StyleSheet::parse` returns `Err`) remain non-recoverable.
The refactoring also consolidates CSS validation logic into a single function, reducing code duplication and making the error handling flow clearer.
## How?
- Created `parse_css_stylesheet()` function that wraps `StyleSheet::parse()` and applies CSS module validation
- Modified error handling in `process_content()` to call the new function and recover from minification errors by re-parsing
- Changed early returns to error emission followed by recovery attempts
- Added test fixtures and e2e tests to verify CSS parse error recovery works correctly
- Added snapshot tests showing expected output when CSS contains parse errors (valid rules are preserved, invalid ones are skipped)
- Skipped CSS parse error recovery e2e tests for webpack (webpack has its own CSS pipeline)
## Test Plan
- Added e2e tests in `css-parse-error-recovery.test.ts` that verify:
- Pages with CSS parse errors still render when using `turbopackIgnoreIssue` config
- Parse errors are properly reported in CLI output
- Added snapshot tests showing CSS output with parse errors, including modern CSS features (`::highlight`, `scroll-marker-group`, `oklch()` color conversion)
- Existing CSS tests continue to pass
This enables server HMR (formally, this is called server fast refresh)
by default for app router pages. For now, it's still disabled for routes
and middleware even with app router.
- Replaces `--experimental-server-fast-refresh` with
`--no-server-fast-refresh` to opt out
- Updates telemetry to record `feature.serverFastRefreshDisabled` when
the disable flag is passed
- [x] Verify `next dev` enables server HMR by default (Turbopack, app
dir, non-edge routes)
- [x] Verify `next dev --no-server-fast-refresh` disables it
- CI
This PR adds `unstable_catchError()` API for a granular custom error
boundary. The error component generated by this API is not a true
component format, as the wrapper provides additional args. Therefore,
the API is a function call by design rather than a boundary component to
avoid merging the `errorInfo` with the user-provided props from the
wrapper.
This API works for both the App/Pages Router. However, `retry()` is not
allowed in Pages Router as it depends on `router.refresh()`, and will
throw. Also, it's exported from `next/error` as there was already an
`Error` component for the [Pages
Router](https://nextjs.org/docs/pages/building-your-application/routing/custom-error#reusing-the-built-in-error-page).
### DevTools
<img width="508" height="90" alt="CleanShot 2026-03-13 at 03 02 46@2x"
src="https://github.com/user-attachments/assets/8b79b1f6-877d-4901-99f7-6a2b4d3e34fe"
/>
Docs to follow up: https://github.com/vercel/next.js/pull/89847
Closes NAR-768
---------
Co-authored-by: Josh Story <story@hey.com>
## What?
When a webpack/turbopack loader produces broken code, error messages now
display **both** the original source and the generated code with source
map information, making it much easier to debug loader issues.
## Why?
Previously, when loaders returned invalid code, error messages only
showed the original source file (after source-map remapping). Users had
no way to see what the loader actually generated, making it hard to
diagnose why the code failed to parse. Showing both sides gives full
context about what went wrong.
## How?
### Turbopack Core (`turbopack-core`)
- **`Source::description()`** — New method on the `Source` trait
providing human-readable descriptions of where code comes from.
Implemented across all source types (`FileSource`, `VirtualSource`,
`WebpackLoadersProcessedAsset`, `PostCssTransformedAsset`, etc.),
producing chains like `"loaders [sass-loader] transform of file content
of ./styles.scss"`.
- **`AdditionalIssueSource`** — New struct to hold a labeled source
location. The `Issue` trait gains an `additional_sources()` method so
issues can expose supplementary code frames.
- **`GeneratedCodeSource`** — A wrapper that strips `GenerateSourceMap`
support from a source, ensuring the *generated* code is displayed as-is
rather than being remapped back to the original.
- **`IssueSource::to_generated_code_source()`** — Helper that detects
sources implementing `GenerateSourceMap` and wraps them in
`GeneratedCodeSource` for display. Used by `AnalyzeIssue` and
`ParsingIssue` to automatically attach generated code frames.
### Error Formatting
- **`turbopack-cli-utils`** — Renders additional sources in CLI issue
output.
- **`format-issue.ts`** — Renders additional sources in the browser
error overlay. Extracted `formatSourceCodeFrame()` helper to deduplicate
code-frame rendering between primary and additional sources.
- Long-line truncation (e.g. minified CSS from SCSS) is handled natively
by the Rust-based `codeFrameColumns` implementation.
### Type Definitions
- Added `SourcePosition`, `IssueSource`, and `AdditionalIssueSource`
interfaces to TypeScript types.
- Updated `PlainSource` (added `file_path`), `PlainIssue` (added
`additional_sources`), and NAPI bindings to pass the data through.
### Test Coverage
- **E2e tests** (`test/e2e/webpack-loader-parse-error/`) with custom
broken JS and CSS loaders, covering all 4 modes:
- **Development (Turbopack)** — Verifies parse errors show both original
and generated code via browser error overlay
- **Development (Webpack)** — Verifies error overlay shows the parse
error (webpack doesn't support additional sources)
- **Production (Turbopack)** — Verifies build failure output with full
error extraction and inline snapshots
- **Production (Webpack)** — Verifies build failure output with inline
snapshots
- Updated `test/development/sass-error/` snapshot to include the new
generated code frame for minified SCSS output.
### Example Output
When a loader produces broken code, users now see:
```
⨯ ./app/data.broken.js:3:1
Parsing ecmascript source code failed
1 | // This file will be processed by broken-js-loader
2 | // The loader will return invalid JavaScript with a source map
> 3 | export default function Data() {
| ^
4 | return <div>original source content</div>
5 | }
6 |
Expected '</', got '{'
Generated code of loaders [./broken-js-loader.js] transform of file content of app/data.broken.js:
./app/data.broken.js:3:46
1 | // Generated by broken-js-loader
2 | export default function Page() {
> 3 | return <div>this is intentionally broken {{{ invalid jsx
| ^
4 | }
5 |
Import trace:
Server Component:
./app/data.broken.js
./app/page.js
```
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <lukeisandberg@gmail.com>
### What?
Fixes the "Ready in" printout for the Next.js dev server after restarts.
### Why?
When the dev server restarts (e.g., due to `next.config` changes), the
"Ready in" time was incorrect, showing the total process uptime instead
of the actual startup duration for the newly restarted server instance.
This was because `NEXT_PRIVATE_START_TIME` was not reset.
### How?
`process.env.NEXT_PRIVATE_START_TIME` is now updated to
`Date.now().toString()` in `packages/next/src/cli/next-dev.ts` right
before `startServer()` is re-called when handling a `RESTART_EXIT_CODE`.
This ensures the "Ready in" time accurately reflects the startup
duration from the point of restart.
The tricky part has been writing a test, that both catches a regression,
and won't be flaky on CI. After some back and forth, I think the Vercel
bot suggestion is probably the simplest.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Joseph Chamochumbi <joseph.chamochumbi@vercel.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: icyJoseph <sephxd1234@gmail.com>
### What?
Two fixes for the Turbopack build tracing introduced in #90397:
1. **Don't block SSG on Turbopack shutdown**: `workerMain()` no longer awaits the shutdown promise before returning. Trace event collection is deferred to `waitForShutdown()`, which the parent process awaits *after* SSG completes. This allows static generation and Turbopack persistence/cache-flush to run in parallel.
2. **Add persistence spans to `trace-build` allowlist**: `turbopack-build-events`, `turbopack-persistence`, and `turbopack-compaction` are now included in the `to-json-build.ts` allowlist so they appear in `.next/trace-build`.
### Why?
- The `await shutdownPromise` in `workerMain()` was too eager — it prevented the caller from acknowledging the build as complete and starting SSG until Turbopack persistence finished flushing to disk.
- The persistence/compaction spans emitted by Rust (`turbopack-persistence`, `turbopack-compaction`) were not in the `to-json-build.ts` allowlist, so they were silently filtered out of `.next/trace-build`.
### How?
**`impl.ts` (worker)**:
- Removed `await shutdownPromise` from `workerMain()` — it now returns build results immediately
- `waitForShutdown()` now returns `{ debugTraceEvents }` after awaiting shutdown, so trace events are collected only after all compilation events (including persistence spans) have been processed
**`index.ts` (parent)**:
- Moved `recordTraceEvents(debugTraceEvents)` from the `workerMain` result handler into the `shutdownPromise` `.then()` chain, so events are replayed into the parent reporter after shutdown completes
**`to-json-build.ts`**:
- Added `turbopack-build-events`, `turbopack-persistence`, `turbopack-compaction` to the allowlist
**Test updates**:
- Enabled `turbopackFileSystemCacheForBuild: true` in the trace-build test fixture
- Updated the Turbopack inline snapshot to include `turbopack-build-events`
Replaces the segment-path-matching scroll system with a simpler model
based on a shared mutable ScrollRef on CacheNode.
The old system accumulated segment paths during navigation and matched
them in layout-router to decide which segments should scroll. This was
necessary when CacheNodes were created lazily during render. Now that we
construct the entire CacheNode tree immediately upon navigation, we can
assign a shared ScrollRef directly to each new leaf node. When any
segment scrolls, it flips the ref to false, preventing other segments
from also scrolling. This removes all the segment path accumulation and
matching logic.
Fixes a regression where calling `refresh()` from a server action
scrolled the page to the top. The old system had a semantic gap between
`null` (no segments) and `[]` (scroll everything) — a server action
refresh with no new segments fell through to a path that scrolled
unconditionally. The new model avoids this: refresh creates no new
CacheNodes, so no ScrollRef is assigned, and nothing scrolls.
Repro: https://github.com/stipsan/nextjs-refresh-regression-repro
There is extensive existing test coverage for scroll restoration
behavior. This adds one additional test for the server action refresh
bug.
Previously, MPA page loads (reload, anchor navigation) while the instant
navigation lock was held would skip hydration entirely and hard-reload
when the lock released. This meant the page was non-interactive during
the locked period and tests couldn't assert on the hydrated React tree.
Now the page actually hydrates from the static shell. The server injects
a script that fetches the static RSC payload, and the client uses
createFromFetch with allowPartialStream so React can hydrate without
erroring on the incomplete Suspense boundaries. When the lock is
released, a soft refresh fetches the dynamic data (falling back to hard
reload if the router hasn't initialized yet).
Also refactors the cookie protocol to encode the lock state as a JSON
array (`[0]` = pending, `[1, null]` = captured MPA, `[1, {}]` = captured
SPA) for better integration with the devtools UI. This is somewhat
entangled with the hydration changes so they're grouped together, but
can be reviewed as atomic commits.
Updates the Instant Navs devtools functionality to use a panel and show
you two options for capturing the SSR shell or the prefetch for a Client
navigation.
Replaces current functionality where clicking Instant Navs changed the
devtools indicator to a blue pending state awaiting the user's next
navigation.
https://github.com/user-attachments/assets/de5c5a40-e836-4719-9517-6d5c6b0aef2c
### What?
Implements dependency-level HMR accept and decline for Turbopack,
covering both ESM (`import.meta.turbopackHot`) and CJS (`module.hot`)
modules.
Previously Turbopack only supported self-accept (`module.hot.accept()`
with no arguments) and self-decline. This PR adds the full
dependency-targeted API:
- `module.hot.accept(dep, cb)` / `import.meta.turbopackHot.accept(dep,
cb)` — single dep
- `module.hot.accept([depA, depB], cb)` /
`import.meta.turbopackHot.accept([depA, depB], cb)` — array of deps
- `module.hot.decline(dep)` / `import.meta.turbopackHot.decline(dep)` —
single dep
- `module.hot.decline([depA, depB])` /
`import.meta.turbopackHot.decline([depA, depB])` — array of deps
### Why?
Libraries like `react-refresh` and user code often need to accept
updates for specific dependencies rather than self-accepting the entire
module. Without this, those HMR patterns fall back to full page reloads
in Turbopack.
### How?
**Runtime** (`turbopack-ecmascript-runtime`):
- Extended `HotState` with `acceptedDependencies`,
`acceptedErrorHandlers`, and `declinedDependencies` maps (keyed by
`ModuleId`)
- Updated `hot.accept()` and `hot.decline()` to handle string and array
signatures
- Updated `getAffectedModuleEffects` to check accepted/declined
dependencies when propagating updates through the module graph
- Added `'declined'` effect type that throws an `UpdateApplyError`
- Track `outdatedDependencies` (map of parent → set of updated deps)
alongside `outdatedModules`
- In the apply phase, invoke per-dependency accept callbacks with the
correct outdated deps
- Use `Set` for `outdatedDependencies` dedup (O(1) vs O(n) lookups)
**Compiler** (`turbopack-ecmascript`):
- Added `ModuleHotReferenceAssetReference` — a single asset reference
type for both accept and decline deps, with shared resolve logic for ESM
(`esm_resolve`) and CJS (`cjs_resolve`)
- Added `ModuleHotReferenceCodeGen` — generates code that replaces dep
string literals with resolved module IDs at compile time
- ESM binding auto-update: when an ESM module accepts a dependency that
it also `import`s, the compiler wraps the accept callback to re-import
the namespace variable (`__TURBOPACK__imported__module__<id> =
__turbopack_import__(<id>)`) before the user callback runs, so ESM
bindings reflect updated values without needing `require()`
- Added `import.meta.turbopackHot` as the ESM equivalent of
`module.hot`, with TypeScript type declarations in
`packages/next/types/global.d.ts`
- Static analysis extracts dep strings from
`module.hot.accept`/`decline` calls; non-analyzable deps emit a warning
with distinct error codes (`TP1204` for accept, `TP1205` for decline)
**HMR gating** (`CompileTimeInfo`):
- Added `hot_module_replacement_enabled` flag to `CompileTimeInfo` to
gate recognition of `module.hot` and `import.meta.turbopackHot` as
well-known objects
- Without this flag, production builds would recognize
`module.hot.accept(...)` and generate HMR-specific code, leading to
runtime errors
- Flag set to `true` for dev servers and `false` for production builds
across Next.js and turbopack-cli entry points
- `import.meta.turbopackHot` getter is only emitted when HMR is enabled
**Server HMR** (`next-api`, `next-core`):
- Server-side `compile_time_info` now also sets
`hot_module_replacement_enabled` so that `module.hot` /
`import.meta.turbopackHot` are recognized during analysis of server
modules
- Server HMR requires the `--experimental-server-fast-refresh` CLI flag;
the flag is passed through `ProjectOptions` to the Rust side so
`server_compile_time_info` only enables HMR when appropriate
**Tests** (`test/development/app-dir/hmr-dep-accept/`):
- ESM single dep accept — verifies parent module is not re-evaluated,
accept callback fires, ESM bindings auto-update
- ESM array dep accept — same as above with `accept(['./dep-a',
'./dep-b'], cb)`
- CJS `module.hot.accept` — pure CJS dep observer pattern with `.cjs`
files
- Single dep decline — verifies full page reload occurs
- Array dep decline — verifies full page reload with
`decline(['./dep-a', './dep-b'])`
### What?
Changes Turbopack's error overlay to show specific SWC diagnostic
messages as the error title instead of generic messages like "Parsing
ecmascript source code failed" or "Ecmascript file had an error".
### Why?
Previously, all SWC parse/analysis errors in Turbopack showed a generic
title (e.g. "Parsing ecmascript source code failed") in the redbox
header, with the actual specific error message buried in the description
below the code frame. This made it harder for developers to quickly
understand what went wrong.
**Before:**
```
Parsing ecmascript source code failed
> 1 | export default () => <div/
| ^
Expected '>', got '<eof>'
```
**After:**
```
Expected '>', got '<eof>'
> 1 | export default () => <div/
| ^
Parsing ecmascript source code failed
```
### How?
**Core change** in
`turbopack/crates/turbopack-swc-utils/src/emitter.rs`:
When the `IssueEmitter` has a `self.title` set (the generic title like
"Parsing ecmascript source code failed"), the SWC diagnostic message is
now used as the issue title, and the generic title is demoted to the
description. When `self.title` is not set, the existing behavior is
preserved (first line of message becomes title, rest becomes
description).
**Test updates** across ~15 test files:
Updated all `isTurbopack` branches in test expectations to reflect the
swapped title/description. Only Turbopack-specific branches were
modified; webpack and rspack expectations are unchanged.
**New test suite** (`test/development/app-dir/ecmascript-error-title/`):
Dedicated tests verifying that both syntax errors (e.g. `Expected '>',
got '<eof>'`) and analysis errors (e.g. `the name 'Table' is defined
multiple times`) show the specific SWC message as the redbox title.
**Turbopack snapshot updates:**
4 snapshot files renamed to reflect new titles (e.g. `Parsing ecmascript
source code failed-*.txt` → `Expression expected-*.txt`).
---------
Co-authored-by: Claude <noreply@anthropic.com>
App router types like routes go through the traditional `require` route
bypassing the Turbopack runtime, so they aren't easily solved with
server HMR right now.
Test Plan: Added an e2e test.
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Previously, we lost stack frame information when server hmr code was applied through `eval`.
This adds and calls \`maybeInlineSourcemap()\` to the Turbopack Node.js HMR runtime that appends \`//# sourceURL\` and \`//# sourceMappingURL\` (base64-encoded) to each `eval`ed module entry.
Test Plan: CI for #90389
This implements the `restart` event, fully evicting all caches. This is needed for when client manifest changes are made.
Test Plan: CI for https://github.com/vercel/next.js/pull/90389
## Summary
Adds the Next.js version as a `version` field on the Turbopack
"initialize project" tracing span. The version is passed from the
TypeScript side (`process.env.__NEXT_VERSION`) through the NAPI bindings
into Rust `ProjectOptions`, where it is recorded on the span.
This makes it easy to correlate trace data with the specific Next.js
version that produced it.
Also standardizes all version reads in `packages/next/src/` to use
`process.env.__NEXT_VERSION` (which is inlined at build time by
`taskfile-swc.js`) instead of `require('next/package.json').version` or
`import { version } from 'next/package.json'`.
### Changes
**Trace span (commit 1):**
- **`crates/next-api/src/project.rs`** — Added `next_version` field to
`ProjectOptions`; recorded as `version` on the `"initialize project"`
span
- **`crates/next-napi-bindings/src/next_api/project.rs`** — Added
`next_version` to `NapiProjectOptions` and wired it through the `From`
impl
- **`crates/next-build-test/src/main.rs`** — Added `next_version` to
test `ProjectOptions` init
- **`packages/next/src/build/swc/generated-native.d.ts`** — Added
`nextVersion` to the TypeScript `NapiProjectOptions` interface
- **`packages/next/src/server/dev/hot-reloader-turbopack.ts`**,
**`packages/next/src/build/turbopack-build/impl.ts`**,
**`packages/next/src/build/turbopack-analyze/index.ts`** — Pass
`nextVersion: process.env.__NEXT_VERSION` when creating the Turbopack
project
- **`test/development/basic/next-rs-api.test.ts`** — Added `nextVersion`
to both `createProject` call sites
**Consistent `__NEXT_VERSION` usage (commit 2):**
- **`packages/next/src/server/dev/hot-reloader-shared-utils.ts`** —
`require('next/package.json').version` → `process.env.__NEXT_VERSION`
- **`packages/next/src/lib/patch-incorrect-lockfile.ts`** —
`nextPkgJson.version` → `process.env.__NEXT_VERSION`; narrowed import to
only `optionalDependencies`
- **`packages/next/src/telemetry/events/swc-load-failure.ts`** — `import
{ version as nextVersion }` → `process.env.__NEXT_VERSION`; narrowed
import to only `optionalDependencies`
## Test Plan
- Verified Rust compilation with `cargo check -p next-api`, `cargo check
-p next-build-test`
- Verified TypeScript types with `pnpm --filter=next types`
---------
Co-authored-by: Claude <noreply@anthropic.com>
Adds an "Instant Navigation Mode" toggle to the Dev Tools indicator
menu. When enabled, navigations only show the cached/prefetched state —
the same behavior as the instant navigation testing API, but toggled
from the dev tools UI instead of test code.
Gated behind `experimental.instantNavigationDevToolsToggle`.
This is a proof of concept — the UX needs more work before it's ready to
ship broadly (e.g. the toggle should be more discoverable, and the
locked state needs clearer visual feedback). But it's sufficient to
demonstrate the workflow and get feedback.
https://github.com/user-attachments/assets/ce2992e1-2794-43d6-baef-d10750d8d8e8
Stacked on #89984
When we initially implemented runtime prefetching we decided to make
sync IO that happens after runtime but before async IO an error. This is
similar to but more aggressive than the error for sync IO before runtime
data but is important because if you have sync IO before uncached IO and
we are runtime prefetching the page then we aren't going to get anything
useful from that prefetch but it might be very expensive.
The problem with the current implementation is that if you turn on
runtime prefetching for a Page segment then in practice the router will
only ever runtime prefetch that page segment but the sync IO validator
will error for sync IO before uncached IO anywhere in the route. This
makes adding runtime prefetching to leaf Segments much harder b/c you
have to address sync IO in the entire Segment stack.
This change updates the sync IO heuristic to only error in segments that
would be runtime prefetched and below. This way when you add runtime
prefetching to a segment you only have to true up the sync IO for the
part of the tree that has the changed behavior.
This PR only partially implements the intended semantics for runtime
prefetching validation of sync IO. One thing that will need to be done
in a follow up is handling sync IO in generateMetadata. In this case
while the functions are defined in layouts and pages they are
semantically always "owned" by the page itself. This means that while
the layout might not be runtime prefetchable and thus allow sync IO
after runtime data access the generateMetadata in that same layout might
not allow sync IO after runtime if there is a runtime prefetch config at
the page level or some layout higher. This will be done in a follow up
Flagged behind `experimental.appNewScrollHandler`
`focus()` on Fragment refs works different than `findDOMNode(this).focus()`.
`findDOMNode` will return an actual host element so it's effectively `HTMLElement.focus()`. If the element isn't focuseable, `focus()` will do nothing. By spec, there's also no way to determine if an element is focusable. It's up to user agents (e.g. sometimes scrollable containers are considered focusable).
`FragmentInstance.focus` will focus the first focusable descendant. That could be a focusable element deep within the fragment e.g. `<Fragment><section><p>A lot of text</p><a href="">Next page</a></Fragment>`. Focusing such a deep element may sometimes be counter productive.
Now we just blur the active element. This is how a hard navigation already works today and ensures we don't skip past content of the new segment.
This means you potentially stay a long way before the actual Segment (e.g. in a preceding nav) with the focus cursor which could be annoying. We could be clever and temporarily insert a focusable element at the start of the Segment, focus it, and then immediately remove. That would leave the focus cursor right before the Segment. However, this requires knowledge about the parent content (can we just insert an element of our choosing?) and relies on knowing what's focusable (a button would be a good bet). That's a bit too clever for a fix without concrete motivation so I'm waiting somebody to complain about the new behavior.
We have two lines of defense:
1. crates/next-custom-transforms/src/transforms/react_server_components.rs validates ESM imports of server-only and client-only and throws an issue if something is wrong. Notably, `require('server-only')` isn't caught by this
2. additionally, we had an `BeforeResolvePlugin` for server-only, client-only and styled-jsx.
We should probably get rid of the first one completely for consistency (it's brittle)
https://github.com/vercel/next.js/blob/79040a318d29641eb3d96a5a47c92a5f8506e826/crates/next-custom-transforms/src/transforms/react_server_components.rs#L663-L674
This PR is about improving the second one:
The error message isn't ideal yet, but better than before (note: no import trace):
```
./bench/basic-app/app
Invalid import
'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.
The error was caused by importing 'bench/basic-app/app'
```
now
<img width="708" height="386" alt="Bildschirmfoto 2026-01-07 um 17 21 59" src="https://github.com/user-attachments/assets/4d49eff3-4959-4b70-aa47-6f3c10c75588" />