## Summary
This PR introduces the first bounded route-preparation phases used by
Request Insights. It instruments existing matcher and bundler boundaries
without changing their behavior or making the spans default-public
OpenTelemetry data.
- trace development route matching and route preparation while a route
is ensured
- keep production matcher reload as a separate sibling phase so reload
work is not charged to matching
- surface the internal phases as `match route`, `prepare route`, and
`reload route matchers` in Request Insights
- cover cold and warm page, App Route/API, and dynamic-route behavior in
both development bundlers
- preserve the existing matcher results, errors, and public OTel
allowlist
## Verification
- `pnpm test-dev-turbo
test/development/app-dir/request-insights-route-preparation/request-insights-route-preparation.test.ts`
- `pnpm test-dev-webpack
test/development/app-dir/request-insights-route-preparation/request-insights-route-preparation.test.ts`
- `pnpm --filter=next types`
<!-- 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 #
-->
A build error for a glob-matched file with no module type named only the
file,
with no route, call site or import trace — the file never becomes a
module, so
it has no place in the module graph. `import.meta.glob` now also reports
an
error at the call site with the pattern and matched key. Also fixed the
stale
`Read more` link (308s to a page whose anchor changed).
<!-- NEXT_JS_LLM -->
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Previously this was treated as side-effect free:
```javascript
const box = { a: { b: globalThis } }; box.a.b.x = 1;
```
This was an issue introduced by
https://github.com/vercel/next.js/pull/94294.
### What?
Fix a potential deadlock in `scope_and_block` (the CPU fan-out primitive
in `turbopack/crates/turbo-tasks/src/scope.rs`) by routing every job
through a single shared work queue, so completion never depends on a
spawned worker being scheduled.
### Why?
`scope_and_block` runs a batch of jobs across the tokio runtime's worker
threads while the calling thread blocks. Previously, jobs at indices
`1..=WORKER_TASKS` were handed *exclusively* to freshly `handle.spawn`ed
worker tasks and never placed on the shared queue — but the calling
thread only drains the queue, so it could not run those jobs itself.
Each spawned worker runs synchronous code and parks on a
`parking_lot::Condvar` (no `.await`, no `block_in_place`), so once
scheduled it holds its runtime core for the whole scope. When the
runtime has fewer worker threads than host CPUs, or they are already
occupied, those workers may never get a core. Their exclusively-assigned
jobs then never run, `remaining_tasks` never reaches 0, and the caller
blocks forever.
### How?
- **Every job goes on one shared queue.** Spawned helpers are now a pure
optimization that pull from the same queue; they are never assigned a
dedicated job. The calling thread drains the whole queue itself in
`end_and_help_complete`, so liveness never depends on a helper being
scheduled.
- **Runtime-accurate helper cap.** The helper count is
`num_workers().min(number_of_tasks) - 1` (per-scope, from
`Handle::current().metrics()`) instead of a process-global host-CPU
constant.
- **Close via a flag, not a sentinel.** The queue carries a `closed` bit
guarded by the same lock as the jobs. `end_and_help_complete` sets it
and `notify_all`s once; a drainer that finds the queue empty exits when
closed or parks otherwise. This replaces the previous `End` token that
had to be ping-ponged across drainers.
- **Wakeup correctness/perf.** `pick_job_from_work_queue` hands off a
surplus `notify_one` when work remains (parking_lot notifications are
not latched), and the enqueue-time `notify_one` re-wakes a parked helper
— the bootstrap wakeup the hand-off cannot provide from a fully-parked
state, which matters most on thread-limited runtimes.
- The `VecDeque` is presized to the job count so `push_back` never
reallocates under the queue lock.
Added a deterministic regression test
(`test_scope_worker_threads_occupied`): it pins every runtime worker
thread with a synchronous sleep, runs the scope on `spawn_blocking`, and
asserts it completes well before the sleep releases. This fails
(cleanly, via timeout) before the fix and passes after.
Closes NEXT-
* remove the synthetic turbopack-build-events parent, instead use `next
build` as the parent
* ensure that the trace uploader captures persistence and compaction
This simplifies things a bit and may make reporting these traces more
reliable, since they aren't reporting to a span with `0` duration
What?
When a static export is enabled, route handlers require export const
dynamic = 'force-static'. However, this instruction is missing in the
documentation.
Why?
Without export const dynamic = 'force-static', next dev and next build
fail when a static export is enabled.
How?
This PR adds the instruction to correct the usage of Route Handlers on
static exports.
This adds a very basic follow re-exports support to async-imported
modules that are side effect free. For example, it supports:
```javascript
// a.js
export const a = 'A'
// b.js
export const b = 'B' // unused
// barrel.js (pure re-export barrel)
export { a } from './a.js'
export { b } from './b.js'
// index.js
const { a } = await import('./barrel.js')
console.log(a)
```
`apply_reexport_tree_shaking` was moved into `turbopack-ecmascript` as
part of this.
---------
Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
## Bug
When a Server Action skips page rendering, the successful response path
attaches pending revalidations to the response’s `waitUntil` promise.
Error paths, including `notFound()`, did not.
As a result, a forwarded action that called `revalidatePath()` or
`revalidateTag()` and then threw could return its error response without
executing the pending invalidation.
## Fix
Centralize the skipped-render revalidation handling and attach its
promise to successful and error responses.
Actions that continue into page rendering retain the existing behavior,
where revalidations are executed before rendering.
## Test Plan
Add a production regression test that invokes an action which calls
`revalidatePath()` followed by `notFound()` and verifies:
- The fallback route’s page is not rendered.
- The cache handler receives the expected path-tag invalidation.
- Removing the new `waitUntil` attachment causes the test to fail.
## Summary
A fetch action can be dispatched to a parameterized route fallback
without concrete route params. When the deployment adapter also supplies
that route’s postponed state, Next.js throws because postponed state and
fallback params are present together.
These requests only need to execute the action and cannot safely render
the fallback route. This change preserves the Resume Data Cache for
cached reads, discards the React postponed state that cannot be resumed
without concrete params, and consistently skips fallback-route rendering
for successful and failed actions.
## Verification
- Added a `next start` integration test that synthesizes an action-only
fallback request and fails without the fix.
- Added coverage for access fallback errors and direct Resume Data Cache
extraction.
- Added a deployment test using a public browser interaction where an
action is retained across navigation to a route that does not bundle it.
<!-- NEXT_JS_LLM -->
- Adds a `DiskWatcherConfig` object to centralize the recursive mode,
polling, and invalidation reason options.
- **Polling Fix:** Use the non-recursive mode if the polling watcher is
selected, ignoring the OS/platform.
- **Polling Fix:** Treat mtime events as data update events if the
polling watcher is selected (this is what
https://github.com/vercel/next.js/pull/96288 was doing, but this PR only
does it if polling is enabled)
- Only treat the `MetadataKind::Any` event as a data change on macOS.
Fixes#80665Closes#96288
Closes https://github.com/vercel/next.js/issues/96574.
Implements:
> experimental.turbopackMinify accepts per-environment granularity, e.g.
{ server: false, client: true } (restoring serverMinification parity)
I also added the edge runtime in there, even though it isn't too widely
used.
This is mostly just to remove some confusion and clean up unused default
values in some places.
When you run on a backport branch or older tag, you probably never want
to use `canary` and just the version at that point. Running on `canary`
branch will implicitly use the latest `@canary` from NPM.
Since our setup job already checks for the version declared in the
checkout, we should use that everywhere so that we only have to check a
single place to be sure about what version is/will be used.
I'll backport this especially because it'll fix a bug in 15.x where we
ignore the release tag.
pure refactor, no behavioral changes are expected. removes a couple of
places that we had to touch when we add/remove stages:
- `AccumulatedStreamChunks` unnecessarily used names like
`staticChunks`/`runtimeChunks`/... instead of just using
`RenderStage.Static`/`RenderStage.Runtime`/... like we do elsewhere
- `collectStageChunk` had to do a switch over stages to map
`RenderStage.Static` to `staticChunks` etc. after removing this, we can
replace the switch with a loop over `RENDER_STAGE_ADVANCE_ORDER`
- in `StagedRenderingController`, we were unnecessarily listing out all
the stages when initializing `triggers`. replaced with a loop over
`RENDER_STAGE_ADVANCE_ORDER`
- in `StagedRenderingController.shouldTrackSyncInterrupt`, using a range
of stages is a better way to express intent than a `switch`
`AGENTS.md` documented `pnpm new-test -- --args <appDir> <name> <type>`,
but `--args` is a flag of `turbo gen`, not an argument forwarded to the
generator. The `--` separator pushes it past turbo's own parser, so the
command fails with `unexpected argument '--args' found` and the
generator never runs non-interactively. That blocks the instruction a
few lines above it, which states that generating tests with `pnpm
new-test` is mandatory.
The correct form passes `--args` directly, without the separator. This
change updates both places that carry the command: the non-interactive
generation block, and the quick smoke testing note under "Test Gotchas".
These properties were previously typed as always set in
`NextConfigComplete`. But in reality, they can be optional.
```
| 'expireTime'
| 'output'
| 'modularizeImports'
| 'allowedDevOrigins'
| 'adapterPath'
```
I couldn't find a better way to fix this than listing them out
explicitly
<!-- 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 #
-->
## What
Updates the [Designing view
transitions](/docs/app/guides/view-transitions) guide so the
`vercel-react-view-transitions` skill points to its **skills.sh** page
instead of the GitHub source, and reframes the sample prompt as an
example.
## Changes
- Link the skill to
`https://skills.sh/vercel-labs/agent-skills/vercel-react-view-transitions`
instead of the `github.com/vercel-labs/agent-skills/...` source tree.
- Use the skills.sh install form (`npx skills add
vercel-labs/agent-skills --skill ...`), consistent with the other skills
referenced in the docs.
- Frame the prompt as "a prompt like this" and add a sentence making
clear the example is not required, readers can prompt for whatever they
want to achieve and the skill guides the agent through the relevant
patterns.
## Why
The skills.sh page is the canonical place to view and install the skill.
The old wording read as if the one prompt was the only way to invoke the
skill.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The weekly `test-e2e-project-reset-cron` workflow never defined
`VERCEL_TURBOPACK_TEST_TEAM` or `VERCEL_TURBOPACK_TEST_TOKEN`, even
though `run-e2e-test-project-reset.mjs` iterates over all three deploy
test teams. Because `resetProject` defaulted `teamId` and `token` to the
base team, and a destructuring default fires on an explicit `undefined`,
the turbopack iteration silently resolved to `vtest314-next-e2e-tests`.
The cron has therefore been deleting and recreating the base team's
project twice per run while never resetting the turbopack team's
project, and reporting success throughout. This dates back to #89458,
which wired the env pair into `build_reusable.yml` and
`test_e2e_deploy_release.yml` but missed the cron.
This drops the defaults in favor of explicitly passing the team.
- new guide, Auth w/ Cache Components
- iron-session example w/ Cache Components
TODO:
- [x] opt-out from the instant requirements
- [x] e2e for the example, using instant helper
---------
Co-authored-by: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com>
Co-authored-by: Aurora Scharff <aurora.sofie@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`@vercel/devlow-bench` was supposed to start being published again in
#96860, but something's going wrong there and it broke the publishing
workflow
I think our npm token currently doesn't allow publishing that package
A build that matched a stylesheet through `import.meta.glob` failed with
"non-ecmascript placeable asset" naming neither CSS nor the stylesheet,
and an
import trace listing the page twice. The message now names the offending
module; the duplicate came from the glob's virtual module ident having
no
layer, which is now set to the importer's layer so trace simplification
can
collapse it (and so the virtual module is distinct per layer).
<!-- NEXT_JS_LLM -->
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
`TurbopackModuleType` has listed `'text'` for a while, but the config
schema
and `ConfiguredModuleType::parse` both rejected it. It now maps to the
existing
`TextSourceTransform` (the same thing `import … with { type: 'text' }`
uses).
`'json'`, which Rust already accepted, is added to the TypeScript type
and
schema too.
`type: 'raw'` has always been documented as "Return raw contents as
string",
but it mapped to an opaque module with no exports instead — that
mismatch is
why `import * as ns from './alpha.md'` with `{ '*.md': { type: 'raw' }
}`
evaluated to `undefined`. `raw` is now an alias of `text`: both run the
file
through `TextSourceTransform` and export its contents as a string.
Neither
name is deprecated; `text` remains the descriptive spelling that matches
the
`import … with { type: 'text' }` import attribute, `raw` keeps working
exactly
as documented. No configuration value maps to the opaque module type
anymore;
`type: 'asset'` remains the way to emit a file and get its URL.
This is also what makes Vite's `?raw` workable: Turbopack has no
built-in
`?raw` handling, so a `{ condition: { query: '?raw' }, type: 'text' }`
(or
`'raw'`) rule is what turns those files into strings. Docs previously
implied
`?raw`/`?url` worked standalone; that's corrected, and the module type
table
is completed and describes `raw`/`text` as equivalent.
Reading a binding off a module that genuinely can't be placed in an
ECMAScript chunk (a stylesheet, a native addon, …) used to evaluate to
`undefined` with no diagnostic; it now reports an error naming the
module.
Side-effect-only imports still work.
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Noticed we were using `babel-plugin-react-remove-properties` but not
seeing the warning. This adds the cases to the switch, just like the
`styled-components` case.
### What?
Fixes styled-jsx styles being missing from the server-rendered HTML of
Pages
Router apps built through a build adapter (what a Vercel deployment
uses),
when the app has its own `styled-jsx` dependency.
- `crates/next-api/src/next_server_nft.rs`: extracts
`styled_jsx_require_hook_modules()` and adds an accurate comment to the
`is_using_adapter` early return (it no longer claims no NFT tracing is
needed - see "Why?").
- `crates/next-api/src/project.rs`: `Project::additional_traced_modules`
now
also returns those modules, alongside the existing `cacheHandler`/
`cacheHandlers` entries.
- `packages/next/src/server/require-hook.ts`: extracts
`styledJsxRequireHookEntries()` (the JS-side equivalent, used by
webpack)
and replaces the silent `catch (_) {}` around registering the aliases
with
a diagnostic warning (still never throws).
- `packages/next/src/build/adapter/build-complete.ts`,
`packages/next/src/build/collect-build-traces.ts`: use that helper
instead
of re-deriving the same resolution inline. No behavior change for
webpack.
- `test/production/adapter-styled-jsx/`: new regression test.
### Why?
The Pages Router renderer and user code have to share a single
`styled-jsx`
module instance: `render.tsx` creates the style registry and hands it to
user code through a React context owned by that specific module
instance. If
user code ends up with a *different* `styled-jsx` instance - which
happens as
soon as the app depends on a `styled-jsx` version that doesn't dedupe
with
Next.js' own pinned version - `JSXStyle` silently renders nothing during
SSR
(`if (!registry) return null`). The `jsx-*` class names are still
emitted by
the transform, so the only visible symptom is a flash of unstyled
content:
the CSS only gets inserted client-side after hydration.
Turbopack keeps `styled-jsx`/`styled-jsx/style` external in the pages
server
bundle, so the single-instance guarantee is established at **runtime**
by
`next/dist/server/require-hook`, which resolves those requests to
Next.js'
own copy from its own install location via a plain `require.resolve()`.
Nothing in any module graph references the files that resolves to (user
code
only ever references its own copy), so output tracing has to add them
explicitly, or the deployment is missing the file, the hook's
`require.resolve()` throws, the aliases are (silently, since #89402)
never
registered, and the two `styled-jsx` instances drift apart.
That explicit tracing existed only for the whole-app
`next-server.js.nft.json` / `next-minimal-server.js.nft.json` - and
those are
not generated at all when a build adapter is used, because an adapter
assembles its output from each endpoint's own NFT instead
(`is_using_adapter` early return in `next_server_nft_assets`). Its
comment
said adapters "don't need any server NFTs" - true for those two
whole-app
files, but not for the styled-jsx entries they also carried, which had
no
other way into an adapter build. Reproduced against a published
`next@16.3.0` release: a plain `next build` with an adapter never
shipped
`node_modules/next/node_modules/styled-jsx/style.js`, nothing deleted or
hand-edited.
### How?
`Project::additional_traced_modules` is Turbopack's existing mechanism
for
"trace this into every endpoint even though nothing references it" - it
already carries `nextConfig.cacheHandler`/`cacheHandlers` for exactly
the
same reason (a runtime-only dependency invisible to static analysis).
Adding the require hook's styled-jsx modules there gets them into every
endpoint's own `*.nft.json` via the existing `trace_endpoint` plumbing,
which
`build-complete.ts` already loads unconditionally for both bundlers - so
no
TypeScript change is needed on the Turbopack side at all.
`styled_jsx_require_hook_modules()` centralizes the resolution (used by
both
`additional_traced_modules` and the pre-existing whole-app NFT, which
still
needs it independently for `output: 'standalone'`).
Webpack has no equivalent Rust-side tracing, so `build-complete.ts`
keeps
tracing these itself via `nodeFileTrace`, now through the shared
`styledJsxRequireHookEntries()` TS helper instead of re-deriving the
same
resolution inline.
The new test builds a Pages Router page with `<style jsx>` through a
build
adapter fixture (same shape as `test/production/adapter-root`), with a
`styled-jsx` dependency version that doesn't dedupe with Next.js' pinned
version, and asserts the adapter's `onBuildComplete` output declares
every
file the require hook resolves.
Verified end-to-end against a published `next@16.3.0` install (the
sandbox
can't build the native Turbopack addon):
- an existing, unmodified mechanism (`cacheHandler`) was used to confirm
the
endpoint-NFT -> adapter-assets pipeline this fix relies on actually
carries
a traced module through to the deployment;
- the Rust change's exact output was simulated at the point
`build-complete`
loads each endpoint's NFT, and the SSR HTML went from 0 `<style
id="__jsx-...">` tags (with `jsx-*` classes still present - the reported
flash) to the expected styled-jsx CSS being present;
- reverting the simulation reproduces the failure again;
- webpack output (`next/node_modules/styled-jsx/{index,style}.js`,
`node-environment`, `require-hook`) is unaffected.
- `cargo check` / `cargo clippy` / `rustfmt --check` pass on the Rust
change.
- [x] Tests added (`test/production/adapter-styled-jsx/`)
- [x] Errors have a helpful link attached — n/a, no user-facing error
added (warning only)
---------
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Follow-up to #96860, addressing @lukesandberg's review.
That PR moved `devlow-bench` into `packages/`, where the root eslint
config applies for the first time, and exempted the package from the
three rules it didn't satisfy rather than fixing the violations.
This drops the exemption block:
- **`import/no-extraneous-dependencies`** — had no violations. The
exemption was unnecessary.
- **`@typescript-eslint/consistent-type-imports`** — 13 violations, all
autofixable.
- **`@typescript-eslint/no-shadow`** — 17 violations, renamed by hand.
The shadow fixes are mostly loop variables and callback parameters that
restated their outer binding. Two worth a look:
- `table.ts` — `getValue(data, ...)` took a single entry but shadowed
the `data` array it indexes into. Renamed the parameter to `entry`, and
the `row`/`column` grouping keys to `rowKey`/`columnKey` so the loop
variables can keep the shorter names.
- `shell.ts` — `command(command, args)` shadowed the exported function
with its own first parameter. Renamed to `executable`, which also fixes
the `[SHELL]` debug log that was interpolating the function rather than
the argument.
## Verification
- `eslint` clean under both the root config and the package's own `lint`
script
- `tsc` compiles, `pnpm run --filter=devlow-bench test` 3/3
- prettier clean
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
## Summary
`@vercel/devlow-bench` hasn't published since December 2024. The job
that published it was removed in #93064; it was gated on the head commit
message starting with `chore: release turbopack npm packages`, which
squash-merge rewrites, so it couldn't fire through normal review —
#93951 bumped the version correctly and still published nothing.
Rather than rebuild that machinery for `turbopack/packages/*`, this
moves the one remaining package to `packages/`, where lerna and the
existing recursive publish already handle everything.
**`publish-release.js` is unchanged.**
The tradeoff is that the package joins the Next.js lockstep version,
going from `0.4.0` to the current canary, and ships nightly instead of
on manual bumps. At ~10k downloads/month, essentially all CI, nobody is
reading its version for compatibility — not worth ~300 lines of bespoke
release code in the script that gates Next.js releases.
`packages/` already holds unscoped, non-`@next/` packages
(`create-next-app`, `eslint-config-next`, `next-rspack`), all lockstep.
The root eslint config covers `packages/` but never applied to
`turbopack/`, so this exempts the three rules the source doesn't satisfy
and prefixes six unused parameters, rather than reformatting the package
as part of a move.
## Deletions
Three nft packages, last published in 2022, none with a build script or
any consumer:
- `@vercel/experimental-nft` — a stub containing only a `package.json`
- `@vercel/webpack-nft` — resolved binaries built from a
`node-file-trace` crate that no longer exists, so its core codepath
couldn't work
- `@vercel/experimental-nft-next-plugin` — depends on the above
`turbopack/xtask/src/publish.rs` goes with them: its only caller was the
workflow removed in #93064, its `npm` subcommand targeted that same
missing crate, and nothing invokes `cargo xtask`.
`turbopack/packages/` no longer exists.
## Verification
- `publish-release.js --dry-run` — publishes `@vercel/devlow-bench` via
the untouched recursive publish
- `lerna list` and `turbo run build` both resolve it at the new path
- `pnpm run --filter=devlow-bench test` — 3/3, and the CI jobs reference
it by name, not path
- Root eslint and the package's own `lint` script both clean
- `cargo check`/`clippy`/`fmt -p xtask`
- Not run: a real publish, which only CI can exercise
`pnpm-lock.yaml` is hand-edited to repoint the workspace entry and drop
the three removed ones. A full `pnpm install` rewrites ~1800 unrelated
lines, and `--frozen-lockfile` fails identically on unmodified `canary`
from pre-existing `apps/docs` drift.
<!-- NEXT_JS_LLM -->
The previous PR in this stack introduced TransportData, the unified
response format, but produced it via a temporary adapter layer that
converted from the old data formats — FlightRouterState and
CacheNodeSeedData trees, encoded as FlightDataPath entries. As promised
there, this PR updates the server to produce the new format directly and
deletes the adapter. Neither FlightRouterState nor CacheNodeSeedData
appears in a rendered response anymore; FlightRouterState survives only
on the client (router state, history) and as the request tree the client
sends to the server, and CacheNodeSeedData is deleted from the codebase
entirely.
createComponentTree now returns the response's transport tree: each node
carries its segment identity, its prefetch hints, and its render output,
constructed in place as the tree renders. When a non-PPR prefetch stops
at a loading boundary, the subtree below the cut is emitted as
structure-only nodes with no render output — the same shape the client
already interprets as "fetch lazily".
createFlightRouterStateFromLoaderTree is replaced by
createTransportTreeFromLoaderTree, which covers the cases where nothing
is rendered: router-state-only responses, route tree prefetches, the
structure below a loading-boundary cut, and error payloads. The prefetch
hints computation is shared with createComponentTree through
computeSegmentPrefetchHints so the two producers cannot drift.
walkTreeWithFlightRouterState returns the transport tree those producers
build: each emit is a transport node, and the levels above it become
"skipped" nodes (position acknowledged, no output attached). The
FlightDataPath encoding this replaces — repeating [segment,
parallelRouteKey] prefixes terminated by a positional data tuple, plus
the hack in generateDynamicRSCPayload that sliced the root segment off
every path — is deleted, along with the FlightData, FlightDataPath, and
FlightDataSegment types.
This also deletes the overriddenSegment / canSegmentBeOverridden
mechanism, which turned out to be dead code: it could only trigger if a
dynamic segment reached the walk as an uninterpolated string, but
getDynamicParam throws in that case rather than returning null.
Instant validation rebuilds its payload tree natively as well: the
builders walk the original payload's transport tree in parallel with the
loader tree, taking structure from the former and render output from the
validation segment cache.
Introduces a new type, TransportData, to replace FlightDataPath. The new
format is the same regardless of whether the entire page is rendered by
the server or if some segments are intentionally omitted.
The type also unifies FlightRouterState and CacheNodeSeedData into a
single tree that includes both route information and RSC data.
Previously these were sent in two separate trees that were isomorphic by
convention, requiring the consumer to walk both in parallel.
These new types are intended to be transport formats only. The client
already has its own, richer data structure called RouteTree. The
transport format is converted into the client format at the network
decoding boundary.
This does not remove FlightRouterState from the codebase entirely; it's
still used both for issuing requests to the server
(Next-Router-State-Tree) and for tracking state in the browser's history
object. A future refactor may replace it for the purpose of issuing
requests, but it will likely survive as the format for tracking state in
history.
This PR does not yet update the server to produce TransportData
directly; a temporary adapter layer is used to convert from the old data
formats to the new format. This intermediate step will not land on its
own; the next PR in the stack will both update the server and delete the
temporary adapter layer.
The client used to represent a server response as two parallel trees: a
RouteTree describing the route structure, and a CacheNodeSeedData tree
carrying the rendered output for each segment. The two are meant to be
isomorphic, but nothing enforced that — every consumer walked them in
lockstep and had to defend against mismatches between them.
This PR adds a `data` field to the RouteTree type and makes it generic
over a per-segment payload. For a server response, the type is
RouteTree<RSCSegmentData | null>. This removes the need to pass a
separate CacheNodeSeedData through the client navigation algorithm in
ppr-navigations.ts.
This is a step toward replacing the RSC response transport format: with
the client consuming a single unified tree, the wire format can move to
one as well.
### What?
Replace `DetachedPromise` with the shared `createPromiseWithResolvers`
helper and use that helper in the vendored `@mswjs/interceptors` Node 20
shim.
This can go away entirely when we support Node 22 at minimum.
### Why?
Next.js maintained duplicate deferred-promise implementations in
`src/lib/detached-promise.ts`,
`src/shared/lib/promise-with-resolvers.ts`, and the interceptor bundle
entry.
### How?
- Migrate `DetachedPromise` consumers to `createPromiseWithResolvers`
- Delete the duplicate `DetachedPromise` implementation
- Delegate the interceptor's required global polyfill to the shared
helper
- Update the generated interceptor bundle and server trace snapshot
Validated with `pnpm --filter=next types`, focused Jest tests, ESLint,
Prettier, and the `ncc_mswjs_interceptors` bundle task.
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: Sebastian "Sebbie" Silbermann <sebastian.silbermann@vercel.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Gets rid of of all usages of the static `GH_TOKEN_PULL_REQUESTS` token
that was issued for `vercel-release-bot`.
We already create commits with `nextjs-bot`. `nextjs-bot` already had
permissions to open PRs which is already being used by React sync.
## Summary
The Instant Navigation testing API is only supported with Cache
Components, but its testing cookie could previously force a non-Cache
Components route into the legacy PPR rendering path. This can cause
requests to fail because that path relies on React’s deprecated
`unstable_postpone` API, which is unavailable in the stable React
runtime.
This change couples testing API exposure and its client bundle machinery
to Cache Components, removes the testing-only PPR capability override,
and adds regression coverage confirming that the API remains inactive
without Cache Components. Supported Instant Navigation behavior is
unchanged.
In dev, `"use cache"` entries are keyed by an HMR refresh hash so that
edits invalidate cached data.
This hash was managed wrong in both directions:
- webpack refreshed the hash at moments when no code changed: on dev
server startup, and when a page file was added or removed. Each refresh
threw away all cached data for nothing.
- The hash never distinguished one dev server run from another. That
matters with a custom cache handler that persists entries: a restarted
server would serve data cached by the previous run, whose code may have
changed while the server was down.
To fix this:
- Adding or removing a page no longer refreshes webpack's hash. Edits
and env file changes still do.
- The hash now includes a random per-run value.
This fixes the `cache-components-dev-warmup` flakes.
## How to reproduce
Here's the first problem in action, on webpack. Render a cached
`Math.random()` on a page, then create an unrelated page file:
```
value: 0.199426
value: 0.199426
$ echo '...' > app/unrelated/page.tsx # never visited
value: 0.990973 # cached entry gone a second later
```
Adding the page threw the cached data away.
Starting the dev server throws it away too, once, shortly after boot.
That one is a race: normally the cache is still empty when it fires, so
nobody notices — but if the first page request gets in before it,
everything that request cached is thrown away and the next reload is
silently cold. That race is the cause of the `dev-warmup` flakes on
loaded machines, and it's how I found this.
The hash refresh is specific to webpack, so Turbopack doesn't lose
cached data this way. On Turbopack, these events instead clear the
server's module cache. Whether the next render picks up a fresh module
instance is a race, so module-scope state can reset. Turbopack also has
a separate bug in this area — it advances its hash once per
change-subscription emission, and an added or removed page can produce
an emission without any edit — which #96248, stacked on this PR, fixes
by deriving the hash from compiled output.
## Underlying cause
Next computes a "client router filter" (a bloom filter over the app's
routes, used by the client-side router), and a filter change is treated
like an env file change. An env change means cached data might be stale,
so the reaction is strong: the browser is told to re-fetch, and on
webpack the hash is refreshed, which discards every cached entry. But
the filter is derived from the routes, so it "changes" whenever you add
or remove a page — and once right after startup, because the first
computation has nothing to compare against. Neither makes cached data
stale.
The second problem is the opposite: dev server runs share cache keys.
The hash starts from the same value on every run (unset on webpack,
`"0"` on Turbopack), so a restarted server generates the same keys as
the old one. With the default in-memory handler that's moot, but a
custom cache handler that persists entries will serve entries written by
the previous run. webpack can also hit this after edits: its hash is
derived from compiled output, so re-applying an edit that a previous run
also made lands on that run's keys.
Judging by the code (?), dev cache reuse was only ever meant to last a
single run: every dev cache mechanism is in-memory, and #75474 takes it
for granted that restarting the dev server clears cached content. So I
treat cross-run reuse as a bug. Not sure I got this constraint right —
in particular, #75474 deliberately made reverting an edit reuse
previously cached data, and this PR keeps that working within a run but
stops it from working across runs.
## Fix
Two changes:
- A filter change is no longer treated as an env change. It still
updates the compiled-in filter value (the part that's actually needed),
but it no longer refreshes the hash, and on Turbopack it no longer wipes
the server's module cache. Real env file changes still trigger the full
reaction.
- The hash now starts from a random per-run value on both bundlers. So
it exists before the first request and never matches another run's keys.
The per-run value is deliberately conservative: it also prevents reuse
across restarts where *nothing* changed. Allowing exactly that reuse
safely needs keys derived from the cached function's implementation (the
existing TODO in `use-cache-wrapper.ts`), which would replace the
per-run value.
## Test plan
The first commit adds five tests
(`test/development/app-dir/cache-components-spurious-cache-invalidation`).
They run on both bundlers, except the module-state test, which is
Turbopack-only: on webpack, adding a page recompiles the server bundle,
which replaces the module instances regardless. So Turbopack runs five
and webpack four.
Before the fix, these fail:
```
webpack: ✕ discards use cache entries when a previous run made the same edit
✕ discards use cache entries across dev server restarts (most runs — see below)
Turbopack: ✕ keeps module state when an unrelated page is added
✕ discards use cache entries across dev server restarts
```
The webpack restart test does not fail every run. The start-time refresh
replaces the unset hash with a content-derived one at an unpredictable
moment. When one server run caches before its refresh and the other
reads after its own, the keys don't collide, so there is no cross-run
reuse for the test to catch that run.
The webpack side of the over-invalidation fix is covered by the refetch
count below rather than by a cached-value test: the equivalent test on
Turbopack depends on emission timing, so it lands with #96248, which
removes that dependence.
The same commit also tightens the refetch count in the test from #96250
below: since a page add is no longer an env change, adding a page
refetches an open tab once instead of twice, on both bundlers. Before
the fix, both bundlers fail that expectation (`Expected: 1, Received:
2`).
To reproduce the flake this PR fixes, run the dev-warmup suites under
CPU load (`for i in $(seq 1 12); do yes > /dev/null & done`). Before the
fix, 6–8 of the 11 tests fail per run on webpack (`Prerender` flips to
`Server` on warm reloads). After the fix, all 11 pass. Turbopack passes
before and after. Also still green: `typed-env` (env definitions were
previously only written at boot via the false positive this removes; now
they're written on the initial scan explicitly), `env-config`,
`use-cache-custom-handler`, and `pages-to-app-routing` (it exercises the
router filter update path).
The dev server tells open tabs when a page is added or removed, so a tab
showing a 404 picks up a page you just created, and a tab showing a page
you just deleted falls back to the 404.
Both bundlers computed the list of changed pages wrong, in opposite
directions.
**Turbopack announced pages that hadn't changed.** To find what changed,
it compared the new route list against the entry maps in
`currentEntrypoints`. Those maps key App Router entries by page name
(`/blog/page`), but the route list is keyed by route (`/blog`), so no
App Router route ever matched: every update announced every existing app
route as added and every page name as removed, and starting the server
announced every route once. Each announcement makes every connected tab
refetch. Fixed by comparing the new route list to the previous route
list, which uses the same naming. Starting the server now announces
nothing on either bundler, since the first route list is not a change.
**webpack didn't announce pages that had changed.** It only announced
when `prevSortedRoutes.every((val, idx) => val === sortedRoutes[idx])`
was false. That is a prefix comparison: when a new route sorts after all
existing ones, the old list is a prefix of the new one, and nothing is
announced. In App Router this mostly went unnoticed, because adding a
page was also (wrongly) treated as an env change, which refreshed tabs
anyway; the next PR in this stack removes that. In Pages Router nothing
hid it: the route manifest was never refetched, so client-side
navigation to the new page kept returning 404 until a manual reload.
Fixed by also comparing the lengths.
**What this PR does not fix:** on Turbopack there is a window right
after a page is added where the dev server has already announced it to
tabs, but can't serve it yet (the route isn't in the dev router's route
table until the watcher pass in `setup-dev-bundler` runs). A tab that
reacts inside that window gets the 404 again, and since each page is
only announced once, it stays on the 404 until you reload it by hand.
That bug predates this PR and is being fixed separately.
So that the new App Router tests don't hit this window, each one edits
the added page again once it's confirmed servable. The edit makes the
server announce again, and this time the tab can only get the page. The
trade-off is that these tests don't prove the first announcement alone
updates the tab — the refetch-count test covers how often changes are
announced, and its tab never sits on the added page's 404, so it can't
hit the window. A TODO in the tests says to drop the extra edits once
the underlying bug is fixed. The Pages Router test needs no such edit:
Turbopack updates the route matchers before it announces, and a Pages
Router tab reloads the document instead of refetching.
## Test plan
New tests cover adds, removals, dynamic routes, route groups, and a
dynamic route being shadowed by a more specific page and then
unshadowed. One more counts how often an open tab refetches when a page
is added: it wraps the tab's `fetch` and counts requests carrying the
`next-hmr-refresh` header, which distinguishes a refetch the dev server
asked for from a navigation or a prefetch. It runs on a dev server no
other test shares, because the count depends on what has been compiled
so far.
Before the fix, adding one page makes an open tab refetch:
```
Turbopack: 12 times (every app route re-announced; grows with the number of routes)
webpack: 1 time (the announcement is never sent; only the env-change refetch happens)
```
The correct count is currently 2: one refetch for the announcement, one
for the env-change false alarm. The next PR removes the false alarm and
tightens the expectation to 1.
Before the fix, webpack also fails the Pages Router manifest test (the
manifest update is sent from the same skipped branch).
With the fix, both bundlers pass all ten tests.
## What
Updates docs links whose targets have moved. Found by an automated docs
link audit: every old URL below currently serves a `308` (permanent
redirect), so readers take an extra hop and the links rot as redirect
maps are pruned.
Each replacement is the **final `200` URL** of the redirect chain
(verified with `curl -sIL`), not just the first hop. Only the path
component was rewritten — scheme, host, query strings
(`?utm_source=...`) and fragments are untouched.
| Old path | Now points at |
| --- | --- |
|
`vercel.com/docs/concepts/functions/edge-functions/og-image-generation`
| `/docs/og-image-generation` |
| `vercel.com/docs/frameworks/nextjs` |
`/docs/frameworks/full-stack/nextjs` |
| `vercel.com/docs/observability/otel-overview` |
`/docs/tracing/instrumentation` |
| `vercel.com/docs/concepts/observability/otel-overview/quickstart` |
`/docs/tracing/instrumentation` |
| `vercel.com/docs/edge-config/get-started` |
`/docs/global-config/get-started` |
| `vercel.com/docs/functions/vercel-functions-package` |
`/docs/functions/functions-api-reference/vercel-functions-package` |
| `vercel.com/docs/edge-network/overview` | `/docs/cdn` |
| `vercel.com/docs/storage/vercel-blob` (×4) | `/docs/vercel-blob` |
| `vercel.com/docs/storage/vercel-blob/server-upload` |
`/docs/vercel-blob/server-upload` |
| `vercel.com/docs/storage/vercel-blob/client-upload` |
`/docs/vercel-blob/client-upload` |
| `vercel.com/docs/concepts/image-optimization` |
`/docs/image-optimization` |
| `nextjs.org/docs/app/building-your-application/rendering` |
`/docs/app/getting-started/caching` |
The four Pages Router pages flagged by the audit (`deploying`,
`guides/instrumentation`, `guides/open-telemetry`, `guides/redirecting`)
share their source with the App Router versions via frontmatter
`source:`, so fixing the App Router files covers them.
## One link worth a reviewer's eye
In `use-client.mdx`, the client-server **boundary** link pointed at
`/docs/app/building-your-application/rendering#network-boundary`. That
path 308s to `/docs/app/getting-started/caching`, which is what this PR
uses — but the redirect target is topically unrelated to the
client-server boundary, and no `#network-boundary` heading exists on it
(or anywhere else in `docs/`). If you'd prefer,
`/docs/app/getting-started/server-and-client-components` is the closer
page and I'm happy to switch it (the anchor would have to be dropped
either way).
## Also checked: `viewTransition`
The `next-config-js` reference index on nextjs.org still lists an entry
linking to
`/docs/app/api-reference/config/next-config-js/viewTransition`, which
404s. No change is needed in this repo: that page was intentionally
deleted in 28a9465d4 ("docs: view transitions guide — skill section,
source-audit fixes, flag-removal docs", #96097) as the docs half of the
`experimental.viewTransition` flag removal (#96098), and the guide now
states that view transitions work with no configuration. There is no
surviving config option to repoint at, and no link to it remains
anywhere in `docs/` or `packages/*/src` on `main` — the stale index
entry comes from the nextjs.org versioned (v15) nav manifest, outside
this repo.
## Verification
- `prettier --check` passes on all changed files.
- `alex` (`lint-language`) reports only pre-existing prose warnings on
untouched lines.
- No `next.js` build was run; this is a docs-only, path-only change.
▲ Created with [Vercel
devbox](https://vercel.com/vercel/~/sandboxes/devboxes/1us5zdxyl2nk5ccodexgd9w3f6k2)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Joseph <sephxd1234@gmail.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
### What?
Start a next application after 16.2.0 in a safari 9 which is used in
playstation 4.
The application will error with this message
```TypeError: Unable to delete property```
### Why?
We narrow down the problem to this line
```delete document.documentElement.dataset.dplId ```
in
```packages/next/src/shared/lib/deployment-id.ts ```
It seems to be related of how webkit implemented dataset, as discussed
in this
[thread](https://stackoverflow.com/questions/28973603/unable-to-delete-property-on-safari-when-trying-to-delete-dataset-attribute-in)
### How?
replace
this line
``` delete document.documentElement.dataset.dplId; ```
with
``` document.documentElement.removeAttribute('data-dpl-id');```
if data cannot be deleted , we can remove the attribute.
It should achieve the same with a different syntax
Fixes#94601