Commit Graph

1409 Commits

Author SHA1 Message Date
Benjamin Woodruff ‮ 7b58e5880c Turbopack: Add support for specifying additional roots (#98003)
Full motivation and plan here:
https://app.notion.com/p/vercel/Turbopack-pnpm-Global-Virtual-Store-383e06b059c480579403ddfd71cc2d40?source=copy_link

The goal is to allow `DiskFileSystem` to traverse outside of it's own
root to other configured `DiskFileSystem`s when following symlinks. We
may allow traversal in other situations in the future, but this is
limited to symlink resolution for now.

## Global Virtual Store

The motivation for this is to enable [pnpm's Global Virtual Store
feature](https://pnpm.io/global-virtual-store) (and there are other
package managers doing this, including nub and bun).

We'd expose the ability to manually configure this in `next.config.js`,
but we should also auto-configure ourselves for popular package managers
(or at least make a best effort to do so, the `PNPM_HOME` semantics can
be complicated). The `ignoreIfMissing` option is provided for this
situation: We can configure a bunch of roots automatically, and they
only actually get set up if they exist, the check for directory
existence is cheap.

## NFT changes

This requires a couple extensions to the `*.nft.json` file format:
https://github.com/vercel/next.js/pull/98469

## Related Issues

- #93556
- https://github.com/pnpm/pnpm/issues/14972
2026-09-18 17:08:40 -07:00
Benjamin Woodruff ‮ 469a7c5573 Turbopack: Add symlinks and additional roots to NFT metadata (#98469)
Implements the following extension:

```typescript
export interface NftFileList {
  // File paths relative to the directory containing the `.nft.json` file, or
  // the current root (if inside of `NftAdditionalRoot`).
  //
  // When using webpack, these paths may exist outside of the tracing root. The
  // [`@vercel/next` package ignores these paths][vc-next].
  //
  // When using Turbopack, these paths are all guaranteed to exist within the
  // `turbopack.root` specified or inferred from the `next.config.js` file.
  //
  // [vc-next]: https://github.com/vercel/vercel/blob/%40vercel/next%404.20.5/packages/next/src/server-build.ts#L1022-L1026
  files: string[]
  // Turbopack extension: A parallel array to `files` (same indices and length)
  // with file content hashes. For symlinks, the hash of the path of the target
  // is stored.
  fileHashes?: string[]
  // Turbopack extension: Explicit symlink mapping information.
  //
  // If included, it's safe to assume that if a file is not in `symlinks` that
  // it is not a symlink. If this is an empty array, there are no symlinks.
  //
  // If omitted, the processor of the nft file must call `read_link` on every
  // file to determine if it is a symlink and determine the target path.
  //
  // This field is always included if `NftJson` includes `additionalRoots`, and
  // it is always included on `NftAdditionalRoot`.
  symlinks?: NftSymlink[]
}

export interface NftJson extends NftFileList {
  version: 1
  // Turbopack extension: A hash of the entrypoint that refers to these traced
  // files. This hash only depends on the content of the entrypoint file, and
  // not all of its traced dependencies.
  entryHash?: string
  // Turbopack extension: Paths stored with different base paths, typically
  // outside of the tracing root.
  additionalRoots?: NftAdditionalRoot[]
}

// Turbopack extension: A collection of paths stored with a different base path.
export interface NftAdditionalRoot extends NftFileList {
  // Stable unique identifier provided in the `next.config.js`. This can be used
  // to generate the output path where these files are copied to (e.g.
  // `.additionalRoots/$[name}`).
  //
  // This is guaranteed to use a character set that is valid on most
  // filesystems, and the identifiers are guaranteed to not have overlaps on
  // case-insensitive filesystems.
  name: string
  // A source path on the build machine that the paths in `files` are relative
  // to. The final build output directory should not depend on this path.
  absolutePath: string
  // Always specified on NftAdditionalRoot.
  symlinks: NftSymlink[]
}

// Turbopack extension: Information on a symlink, including which additional
// root it maps to. Symlinks that do not cross root boundaries (the common case)
// omit the index into `additionalRoots`.
//
// It is often complicated to transform raw symlink targets to root-relative
// paths, and including this information here ensures that the NFT reader gets
// the same result that Turbopack's tracing system expects.
//
// Because the link target type is unspecified, on Windows the reader needs to
// call `stat` to determine if a link target is a directory or file.
export type NftSymlink =
  | [
      // Index of `files` that refers to a symlink.
      number,
      // The target path of the link. In `NftJson`, this path is relative to the
      // directory containing the `.nft.json` file. In `NftAdditionalRoot`, this
      // is relative to the current root.
      string,
    ]
  | [
      // Index of `files` that refers to a symlink.
      number,
      // The target path of the link relative to the specified root.
      string,
      // An index into `additionalRoots`, -1 if the target path is relative to
      // the `.nft.json` file's directory,
      //
      // If the symlink target is relative to the same root as the symlink
      // itself (the "current root"), this field is omitted.
      number,
    ]
```

Nothing currently populates `additionalRoots`.
https://github.com/vercel/next.js/pull/98003 will do it.
2026-09-18 17:08:39 -07:00
Jimmy Miller 389f164d3c Fix generated Turbopack GC option docs (#98888)
I was noticing the generated file change every time I ran `pnpm
swc-build-native`. This comment was added
[here](https://github.com/vercel/next.js/pull/98378/changes#diff-0db38a2a3ab56ce5ce08014e3f096a6043c5fedd54a4048d231de4cbaa4ed6f1R512)
in the generated file, but not in the source
2026-09-18 09:39:38 -07:00
Jimmy Miller e6688470ef Properly disable laziness on next/dynamic (#98828)
next/dynamic assumes a eager semantic for css gathering, we are making
sure that holds even when lazy dynamic imports are enabled
2026-09-18 06:59:28 -07:00
Jiwon Choi 5d9ab72cef Add next upgrade --ai and security vulnerability coverage (#98562)
> [!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.
2026-09-18 00:52:20 +02:00
Luke Sandberg bf13ab8ac8 Introduce config options for Turbopack GC (#98378)
### What?

Replaces the Turbopack GC environment variables with an
`experimental.turbopackGc`
config option

```js
// next.config.js
experimental: {
  turbopackGc: true,                                      // on, defaults
  turbopackGc: { minProgressMs: 100, rootTtlMs: 432e6 },  // on, tuned
}
```

This replaces the env vars which were unused and not the ergonomic
2026-09-16 17:16:02 -07:00
Benjamin Woodruff ‮ 02f3bebc63 Turbopack: Remove wasmer from lockfile by deleting dead feature option from our wasm crate (#98761)
Apparently we don't support swc plugins when using the wasm binary, and
this feature flag is never used. It was causing wasmtime to show up in
our lockfile.

Actual impact on compilation time is likely zero, as we never enabled
this feature.
2026-09-16 22:02:29 +00:00
Luke Sandberg 16c8ea5d55 Prefer using join over try_join when you don't want a vec (#98547)
Adopt `join` over `try_join` in a number of places to avoid temporary
Vec allocations

`try_join` always copies a `Vec<Result<T>>` to a `Result<Vec<T>>` which
is sometimes convenient but at least sometimes wasteful if you are
immediately transforming into another collection type.

Adopt it to turn `primary_modules` into a `SmallVec` since in the vast
majority of cases there is only one module and so we can save a bunch of
allocations of tiny arrays

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:46:39 -07:00
Niklas Mischkulnig bdbf63aef7 fix(next-custom-transforms): never place generated imports before directives (#98717)
## Summary

**What?** Fixes the error reported for a module that has a top-level
`"use client"` directive and an inline `"use server"` function
directive, e.g. `<form action={async () => { "use server" }} />` in a
client page.

**Why?** Under Turbopack this reported a bogus error — `The "use client"
directive must be placed before other expressions` — even though the
directive was already at the top of the file. The actual mistake (an
inline server action inside a Client Component) was never surfaced.

**How?** On Turbopack's RSC layer the server actions transform runs
*before* the React Server Components assert. When it hoists an inline
action it prepended its generated imports (`registerServerReference`,
action encryption, cache runtime) at index 0 of the module — *above* the
`"use client"` directive, which the transform does not consume. The RSC
assert then saw an import before the directive and reported it as
misplaced.

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-09-16 17:09:32 +02:00
Tobias Koppers 3dc93da27e Improve tree shaking through Next.js module wrappers (#98638)
## What?

Extends the export-usage passthrough introduced by #98621 to the
transparent wrappers used for dynamic entries, server components, and
shared server utilities.

The existing reference-tree-shaking production fixture now covers each
wrapper and verifies that unused sibling exports do not remain in
Turbopack's server chunks.

## Why?

These wrappers forward another module's export surface, but previously
marked every target export as used. That kept otherwise unreachable
modules in production output and limited inner-graph tree shaking.

## How?

The dynamic-entry and server-component wrappers now pass their own
resolved export usage to their targets. The custom server-utility
reference reports the same passthrough binding usage while preserving
its shared chunking and merge behavior.

A public Turbopack constructor creates passthrough export-usage values
for references outside `turbopack-core`, consistent with the existing
constructors for other usage modes.

## Verification

- `cargo check -p next-core`
- `pnpm build-all`
- `pnpm test-start-turbo
test/production/app-dir/reference-tree-shaking/reference-tree-shaking.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/reference-tree-shaking/reference-tree-shaking.test.ts`

<!-- NEXT_JS_LLM -->


<!-- fleet df1472be-5cd1-4bd6-9fab-4efc1defa4f9 -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-14 17:53:21 +02:00
Tobias Koppers e3ababa0d5 Trace export usage through client component proxies (#98621)
## Summary

Turbopack previously widened export usage to every export when a Server
Component crossed a client-component proxy. As a result, importing one
named export from a `use client` module retained unrelated
implementations in both browser and SSR bundles.

This extends Turbopack's import annotations with a generic export-usage
passthrough. The server transform for `use client` modules marks its
generated namespace import as passthrough, so the ordinary binding-usage
fixed point carries the Server Component's used export set into
`EcmascriptClientReferenceModule`. Its client-reference edges use the
same signal to forward that set to browser and SSR targets.
Side-effect-only evaluation references retain their evaluation semantics
instead of inheriting the annotation. The forwarded names remain
namespace-observable because React Flight resolves client references by
their original export names, so unused exports can be removed without
changing the protocol-visible identity. An importer whose usage is `All`
still forwards `All`.

The client-component tree-shaking test now checks that unused markers
are absent from every browser chunk and verifies the same behavior in
Turbopack SSR chunks.

## Verification

- `cargo fmt --all -- --check`
- `cargo check -p next-core`
- `cargo test -p turbopack-core module_graph::binding_usage_info::tests
--lib`
- `pnpm build-all`
- `pnpm test-start-turbo
test/production/app-dir/client-components-tree-shaking/index.test.ts`
- `pnpm test-start-webpack
test/production/app-dir/client-components-tree-shaking/index.test.ts`

<!-- NEXT_JS_LLM -->


<!-- fleet 10716dab-2d36-4687-94ae-cd7bdfeac985 -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-14 17:53:21 +02:00
Ben 000390a8fc fix: detect proxy.ts correctly with compound pageExtensions (#93246)
## Maintainer status

- Current with `canary` as of 2026-05-18; this branch includes a clean
merge from latest `origin/canary`.
- Lightweight checks pass; full fork workflows are still
`action_required` until a maintainer approves CI.
- No unresolved review threads or failing jobs are reported after the
refresh.
- Review focus: shared convention-file basename extraction for proxy,
middleware, and instrumentation under compound `pageExtensions`; TS and
Rust paths use the same rule.

---

## Summary

Fixes #85648
Fixes #86303
Fixes #91600
Fixes #85646
Related to #86122
Fixes #92342
Closes #92934

When `pageExtensions` is set to compound extensions like `['page.ts',
'page.tsx']`, proxy files must be named `proxy.page.ts`. However, the
proxy detection logic used `file_stem()` (Rust/Turbopack) and
`path.parse().name` (JS/webpack), both of which only strip the **last**
extension — so `proxy.page.ts` becomes `proxy.page` instead of `proxy`,
and the proxy is never detected.

**Root cause:** `path.parse('proxy.page.ts').name` returns
`'proxy.page'`, not `'proxy'`. Same issue with Rust's `file_stem()`
which uses `rsplit_once('.')`.

**Fix:** Use `file_name().split('.')[0]` (JS) /
`file_name().split('.').next()` (Rust) to extract the first segment
before any dot. This correctly returns `'proxy'` for both `proxy.ts` and
`proxy.page.ts`.

The same `fileBaseName` extraction is also used for `middleware` and
`instrumentation` convention file detection, fixing compound
pageExtensions for those as well.

### Changes

- **Turbopack (Rust):** `crates/next-api/src/project.rs` (2 locations) +
`crates/next-api/src/middleware.rs` (1 location)
- **Webpack (JS):** `packages/next/src/build/index.ts` (build-time
detection) +
`packages/next/src/server/lib/router-utils/setup-dev-bundler.ts`
(dev-time detection)
- **E2e tests:**
- `proxy-page-extensions/` — proxy + instrumentation with compound
extensions
- `middleware-page-extensions/` — middleware with compound extensions
(separate fixture because the build refuses both `proxy.*` and
`middleware.*` simultaneously)

### Verified locally

| Mode | Bundler | Result |
|------|---------|--------|
| Dev | Turbopack | PASS (5/5) |
| Dev | Webpack | PASS (5/5) |
| Production (build+start) | Turbopack | PASS (5/5) |
| Production (build+start) | Webpack | PASS (5/5) |

Existing proxy test suites also verified (proxy-runtime-nodejs,
proxy-with-middleware, proxy-missing-export, proxy-runtime) — no
regressions.

## Test plan

- [x] `proxy-page-extensions.test.ts` covers proxy header injection,
page render through proxy, and `instrumentation.page.ts:register()`
running
- [x] `middleware-page-extensions.test.ts` covers middleware header
injection and page render through middleware
- [x] All 5 cases pass in dev/turbopack, dev/webpack, start/turbopack,
start/webpack
- [x] Existing proxy-runtime-nodejs tests pass (dev/webpack,
dev/turbopack, production/webpack)
- [ ] CI passes on all existing proxy tests

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-09-14 17:04:35 +02:00
Tobias Koppers 39f5d797ab Optimize strict module factory chunks (#98421)
### What?

Reduce Turbopack ECMAScript chunk size by emitting strict module
factories in a shared strict-mode scope instead of repeating `"use
strict"` in every factory.

The chunk format now chooses among the existing flat representation, a
mixed representation with flat non-strict factories plus a nested strict
array, and an all-strict representation that wraps the complete chunk in
a strict IIFE. Each factory is generated through the existing single
code path; the production minimizer removes its redundant directive when
the factory is created inside a strict wrapper. Browser and Node.js
emitters share the mode selection and serialization implementation,
while the runtime remains compatible with the existing flat format.

### Why?

ECMAScript modules are always strict, so large chunks currently repeat
the same directive across many factories. Moving strictness to the scope
where those factories are created removes redundant bytes without
copying arrays or changing strict/sloppy execution semantics.

For `bench/basic-app`, this reduces total raw JavaScript by 16,831 bytes
(0.161%). Partitioning factories changes compression behavior, resulting
in a 1,286-byte (0.051%) gzip increase across all JavaScript; format
selection therefore remains based on emitted raw bytes.

### How?

- Determine each chunk item's strictness from the already-collected
chunk metadata.
- Generate every factory once with its normal directive, then rely on
the minimizer to remove redundant directives inside strict wrappers.
- Compare the expected minified directive bytes removed with the exact
wrapper bytes added and keep small chunks in the existing flat format.
- Keep non-strict factories flat in mixed chunks and append strict
factories as an array produced by a strict IIFE.
- Wrap the complete registration/export in a strict IIFE when every
factory is strict, keeping those factories flat and avoiding a nested
array entirely.
- Use the shorter arrow IIFE when the target supports it, with a
function IIFE fallback for older targets.
- Preserve source-map sections, scope-hoisted module IDs, factory
naming, and ordering within each partition.
- Add focused strict, sloppy, mixed, all-strict, old-target, and
below-threshold coverage and update affected Turbopack snapshots.

### Verification

- `cargo nextest run -p turbopack-tests -E 'test(snapshot)'`
- `cargo clippy -p turbopack-ecmascript -p turbopack-browser -p
turbopack-nodejs`
- `pnpm build-all`
- `pnpm --dir turbopack/crates/turbopack-ecmascript-runtime/js
check:nodejs`
- `bench/basic-app` production Turbopack build, repeated to confirm
deterministic size totals

<!-- NEXT_JS_LLM -->


<!-- fleet 528ffe69-eb27-466b-a132-842272a782cf -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-14 12:34:30 +02:00
Will Binns-Smith d5276f04a1 Make TurbopackResult payloads disjoint (#98575)
### What?

Refactors the JavaScript-facing `TurbopackResult<T>` into a stable
wrapper whose payload lives under `value` and whose issues remain
top-level. Native API consumers, entrypoint conversion,
development-server paths, HMR handling, and direct API tests now follow
the disjoint shape.

Two event subscriptions that had inaccurate wrapper declarations now
expose their existing plain runtime payloads explicitly: update-info
events remain `UpdateMessage`, and compilation events remain
`CompilationEvent`. Nullable native entrypoint payloads are also
declared accurately and normalized at the JavaScript API boundary.

### Why?

The previous intersection-based representation merged payload fields
with result metadata. That allowed fields such as `issues` to overwrite
one another and caused non-object payloads to be discarded, making the
result shape depend on `T`. A dedicated payload property avoids those
collisions and preserves every payload type, including `null`.

Keeping plain event streams distinct from result wrappers also ensures
`TurbopackResult<T>` consistently means the native API actually provides
wrapper metadata.

### How?

The N-API serializer now always creates a fresh wrapper rather than
mutating an object payload. The shared TypeScript type models that
wrapper directly and requires an explicit payload type. Conversion
layers replace only the nested payload while explicitly preserving
wrapper issues. Call sites continue to process wrapper issues while
reading domain data through `value`.

The update-info and compilation-event declarations were aligned with
their native callback types instead of introducing new runtime wrappers
for streams that do not collect issues. Rust `Option<NapiEntrypoints>`
payloads are declared as nullable, then normalized to the existing
empty-entrypoints representation after a null-safe route check.

### Verification

- `pnpm build-all`
- `pnpm --filter=next types`
- `pnpm swc-build-native`
- `cargo check -p next-napi-bindings`
- `cargo fmt --all -- --check`
- ESLint and Prettier on changed files
- `pnpm test-dev-experimental-turbo
test/development/app-aspath/app-aspath.test.ts`
- `pnpm test-dev-turbo test/development/basic/next-rs-api.test.ts` — 26
passed, 1 skipped, 15 snapshots; Jest reported lingering open handles
after the green summary
- `pnpm test-dev-turbo
test/development/app-dir/concurrent-install/concurrent-install.test.ts`
- `pnpm test-dev-experimental-turbo
test/development/app-dir/concurrent-install/concurrent-install.test.ts`

<!-- NEXT_JS_LLM -->

<!-- fleet 74688340-f99a-462e-8f75-88d12c88000a -->

---------

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>
2026-09-11 16:25:09 -07:00
Niklas Mischkulnig f41441a0c8 Durable use cache: optimize env var existence checks (#98504)
We now separately track code that accesses non-inlined env vars at
runtime in two categories
- (existing) actual reads, the full value is accessed
- (new) only unset/falsy/truthy is read at runtime

For the second case, we only need to invalidate use-cache functions when
the given env var transitioned betwen unset/falsy/truthy. But
transitioning between two different truthy values doesn't caused
invalidation.
2026-09-11 22:19:32 +02:00
Jimmy Miller 30d6d8f497 Adds this.mode for webpack loaders (#98532)
Pretty straightforward. But a number of loaders depend on it including

- postcss-loader
- sass-loader
- stylus-loader
- vue-loader
- nunjucks-loader
- thread-loader
2026-09-11 12:05:03 -07:00
Jimmy Miller d155ba9ebf [turbopack] Lazily compile dynamic imports in development (client side) (#97203)
## Summary

Defer compiling client-side dynamic import targets in Turbopack
development until the browser requests their manifest chunk. This avoids
compiling untouched dynamic imports while preserving server-side
imports, CSS loading, Server Actions, source maps, and Fast Refresh
behavior.


One caveat on this is next/dynamic still does some eagerness. I looked a
bit at changing things. But it breaks some of the guarantees there and
decided to leave it off the table.

---------

Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-09-11 07:29:43 -07:00
Benjamin Woodruff ‮ c1156571cd Turbopack: Deduplicate project options documentation (#98465)
By documenting both the partial and non-partial versions of these
objects, we were documenting everything multiple times.

Just reference the non-partial object from the partial object's
documentation.
2026-09-10 08:59:49 -07:00
Josh Story e674165f36 Report incompatible parallel route slots (#97430)
With strict route matching a layout can have sibling slots whose pages
can never form a complete route. For example:

```
app/
  layout.tsx
  @left/
    foo/
      page.tsx
  @right/
    bar/
      page.tsx
```

`/foo` is always missing `@right`, and `/bar` is always missing `@left`.
The pages were authored as route targets, but neither URL can construct
the complete slot tree owned by `app/layout.tsx`. Catchall pruning
removes broad matchers that cannot render and the unmatched-page
validation reports dangling files, but neither explains this
project-level mistake.

This validates the finalized ordinary matcher set and reports each
incomplete URL at the layout that owns the conflicting slots. Adding the
corresponding pages or an explicit `default.tsx` makes the topology
valid. Framework-owned defaults do not invent a `children` slot, and
interception matchers remain exempt because they intentionally describe
partial router state updates.

<!-- NEXT_JS_LLM -->
2026-09-10 08:02:34 -07:00
Josh Story dea15ec603 Require canonical routes for interception routes (#97428)
Interception routes only describe the branch used during a soft
navigation, but every URL they can produce still needs to be handled by
an ordinary App Router page matcher for a direct request or refresh.
Today an interception-only URL can appear to work during client
navigation and then 404 when the same URL is loaded directly. This makes
strict route matching reject that configuration.

For example, this setup is incomplete:

```
app/
  @modal/
    (.)photo/
      [...id]/
        page.tsx
  photo/
    [id]/
      page.tsx
```

The ordinary route handles `/photo/1`, but the interception route also
handles `/photo/1/2`, which has no ordinary page matcher. Widening the
ordinary route makes the setup valid:

```
app/
  @modal/
    (.)photo/
      [...id]/
        page.tsx
  photo/
    [...id]/
      page.tsx
```

The validation checks coverage rather than requiring an identical
matcher. A broader catchall can cover the intercepted pattern, and
multiple ordinary matchers can cover different accepted lengths. For
example, `items/page.tsx` together with `items/[...rest]/page.tsx`
covers an intercepted `items/[[...parts]]` matcher. Route handlers and
generated metadata routes do not count as canonical page matchers.

Webpack and Turbopack now apply the same coverage check and report the
interception matcher and the canonical pattern that is not fully
covered. Existing interception fixtures now include semantically
complete ordinary routes instead of relying on interception-only URLs.

<!-- NEXT_JS_LLM -->
2026-09-10 08:02:34 -07:00
Josh Story 7b7cee3086 Error when app pages do not match any route (#97401)
## What?

This reports an error under `experimental.strictRouteMatching` when a
`page.tsx` is left out of every retained matcher. A page can lose its
own broad catchall matcher and still be valid if it participates in a
more specific matcher, so this checks the final matcher set instead of
treating every pruned candidate as an error.

Both Webpack and Turbopack report every unmatched page in development
and production builds.

## Why?

Pages that are never selected by any complete loader tree are
unreachable by construction. This usually means parallel slots define
incompatible paths without matching pages or defaults, and silently
ignoring those files makes the project look like it has routes that
cannot actually exist.

For example:

```text
app/disagreeing-slots/
├── layout.tsx
├── @first/foo/page.tsx
├── @second/bar/page.tsx
└── [...slug]/page.tsx
```

Every candidate is incomplete: `/foo` has no match in `@second`, `/bar`
has no match in `@first`, and URLs handled only by the catchall have no
matching page or applicable default in either named slot. Strict route
matching prunes all three matchers, and this change reports all three
page files instead of silently leaving them out of the build.

The same validation catches a real `children` branch that only becomes
incomplete below its page:

```text
app/declared-children/
├── layout.tsx
├── page.tsx
└── @panel/
    ├── default.tsx
    └── details/page.tsx
```

`/declared-children` is valid, but `/declared-children/details` only has
a page in `@panel`. Because the declared `children` slot has no matching
page or default at `/details`, that matcher is pruned and
`@panel/details/page.tsx` is reported as unreachable. The root
`page.tsx` is still part of the valid `/declared-children` matcher and
is not reported.

A pruned matcher is not automatically an error. If
`@slot/[...parts]/page.tsx` is combined into retained `/foo` and `/bar`
matchers, that page still participates in routes and remains valid.

## How?

Webpack snapshots the authored app page paths before pruning and
compares them with the pages used by the retained app paths. Turbopack
collects authored page source files from the directory tree and compares
them with the files referenced by the final accepted loader trees. The
tests cover disagreeing named slots, declared `children`, optional
catchalls, route groups, nested slots, and the valid case where a pruned
catchall is still used by more specific matchers.

## Verification

- `pnpm test-start-turbo
test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts`
- `pnpm test-dev-turbo
test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts`
- `pnpm test-dev-webpack
test/e2e/app-dir/unmatched-app-pages/unmatched-app-pages.test.ts`
- `pnpm test-start-turbo
test/e2e/app-dir/parallel-routes-pruned-matchers/parallel-routes-pruned-matchers.test.ts`
- `pnpm test-start-webpack
test/e2e/app-dir/parallel-routes-pruned-matchers/parallel-routes-pruned-matchers.test.ts`

<!-- NEXT_JS_LLM -->
2026-09-10 08:02:33 -07:00
Tobias Koppers ec62d6a48f fix(turbopack): add get_relative_request_to for paths used as module requests (#98497)
### What?

Adds `get_relative_request_to`, next to `get_relative_path_to`, for
callers that need a relative
path they can hand to the resolver as a module request. It returns
`./c`, `../c` or `.`, where
`get_relative_path_to` returns `c`, `../c` or `.`.

Fixes `import.meta.glob`'s `base`-relative keys, which currently come
back without the leading `./`:
`{ base: '../outside' }` produces `one.js` where Vite (and our own docs,
and the existing execution
fixture) expect `./one.js`. That fixture is failing on `canary` today;
it passes here unchanged.

### Why?

The `./` prefix is not decoration — it is what makes a string a
*relative* request. Without it, `c`
resolves as the package `c` rather than the file next to `from`.

`get_relative_path_to` stopped emitting the prefix for a path below
`from`, and every caller that
builds a request had to add it back by hand. Eight of them do, each with
the same three lines. That
works, but it puts the requirement in the call sites instead of the API,
and nothing tells a new
caller that it exists — so the ninth request-building call site,
`import.meta.glob`'s `base`-relative
key, was written without it and silently produced the wrong keys. The
two changes were authored
independently and each was green on its own; only together do they
produce the bug, which is why CI
caught it on `canary` rather than on either pull request.

Rather than add a tenth hand-written prefix, this makes "give me a
request" something you can ask
for. The failure mode it removes is the silent one: a caller that
forgets is no longer possible,
because the choice is now the function you call.

### How?

`get_relative_request_to` shares its traversal with
`get_relative_path_to` through one private
helper, so the two can't drift; the only difference is whether a result
that would be bare gets the
prefix. `get_relative_path_to` keeps its signature and behaviour
exactly, including the fast path
that returns `target` by reference when `from` is empty — the request
form can't borrow there, and
the unit tests assert that difference so the fast path isn't reused by
mistake. `FileSystemPath`
gains the matching method.

The eight hand-written prefixes now call the new function and drop their
snippets. Deciding which
call sites to convert needed care, because the callers of
`get_relative_path_to` genuinely want
different things, and the ones that want a plain path deliberately keep
it:

- source-map paths reported back to the client,
- the Next.js template expander, whose invariant check expects the bare
form,
- the paths written into NFT files, which are data rather than requests.

Two locals that hold requests rather than paths are renamed accordingly,
and one leftover
`strip_prefix("./")` is removed — it had become a no-op undoing a prefix
that is no longer produced.

### Testing

The `import-meta-glob-relative` execution fixture already asserts the
correct keys and currently
fails, so the fix is verified by that fixture passing **unmodified** —
its expectations were right
all along.

The unit test that covered these cases before the prefix went away is
restored, now run against both
functions and against `FileSystemPath`, which had been left with no
coverage of either method. Its
original assertions turn out to describe the request form exactly, so
they pin that
`get_relative_request_to` reproduces the earlier contract rather than
merely resembling it.

Beyond that: the full `turbopack-tests` suite, and the `next-server-nft`
and `next-taskless` tests,
which are what would break if a call site that wants a plain path had
been converted by mistake.

<!-- NEXT_JS_LLM -->

<!-- fleet 3acc7e63-52a8-4472-89cd-b1bbb8ad59fa -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-09-10 13:06:38 +00:00
Sebastian "Sebbie" Silbermann 76a0bdbdf4 [scripts] Move scripts (and benchmarks) off of node-fetch (#98347)
We're using Node.js versions with a built-in `fetch` implementation in
all of these scripts.
2026-09-10 13:50:35 +02:00
Tobias Koppers ed1aab5d38 fix(turbopack): resolve ../ and /-rooted import.meta.glob patterns (#96557)
### What?

`import.meta.glob()` now resolves `../`-relative and
`/`-project-root-absolute
patterns (and `base`), validates every pattern (positive and negative)
for
whether it escapes the filesystem root, and shares the underlying
"resolve leading `./`/`../` against a directory" primitive with
`next-core::relativize_glob` (used by `outputFileTracingIncludes`)
instead of
duplicating it.

### Why?

`import.meta.glob()` handed its patterns straight to Turbopack's `Glob`,
which
matches relative to the scanned directory and understands neither a
leading
`/` nor `..` segments. `import.meta.glob('../../content/*.js')` and
`import.meta.glob('/content/*.js')` therefore silently returned `{}`,
and a
`base` that walked above the filesystem root failed with an internal
error
and a panic report instead of a normal diagnostic. A negative
(exclusion)
pattern that escaped the root was silently dropped, which is the worst
outcome for an exclusion — it would silently include files the user
asked to
exclude.

### How?

- Patterns are split into the directory they're rooted in and the
remaining
glob, matched relative to a single common scan directory covering every
  positive and negative pattern. `/` is resolved from the project root,
matching Vite's rule that patterns are relative to the importing file or
  absolute from the project root.
- Map keys follow the pattern too: `base` now produces base-relative
keys
  (`'./zeta.js'` instead of `'../../content/zeta.js'`, matching Vite), a
  project-root-absolute pattern produces project-root-absolute keys, and
  origin-relative keys are unchanged otherwise.
- Every pattern is validated up front; one that leaves the project (a
`base`,
a positive pattern, or a negative one) reports an error issue at the
call
  site and yields an empty object, instead of crashing or being silently
  dropped.
- The `./`/`../`-resolution loop is extracted into
  `turbo_tasks_fs::glob::relativize_glob`, a crate both `next-core` and
`turbopack-ecmascript` already depend on. `next-core::relativize_glob`
is
now a thin wrapper over it (same name, signature and error message); its
5
unit tests are removed since the shared primitive's own tests cover the
  same cases.

<!-- NEXT_JS_LLM -->

Co-authored-by: Niklas Mischkulnig
<4586894+mischnic@users.noreply.github.com>

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-10 08:42:37 +02:00
Benjamin Woodruff ‮ be2c3fdb3a Add a fast-path to 'get_relative_path_to' for a potential common case when 'from' is empty (#98440)
When creating an NFT file in
https://github.com/vercel/next.js/pull/98003, I store a generic list of
"base" paths and then construct the final path in the JSON file as
relative to the base path. For most files, these paths are relative to
the `*.nft.json` file, but for additional roots, this base path is
always an empty string (relative to the root of the filesystem), so
being able to re-use the original `RcStr` instead of constructing a
brand new string may be beneficial.

This also moves the unit tests to be colocated next to the functions
that are actually being tested.
2026-09-09 15:54:55 -07:00
Jimmy Miller b879776299 Adds this.target for webpack loaders (#98436)
There are a bunch of different loaders I found depending on this

- vue-loader
- svelte-loader
- svg-sprite-loader
- nunjucks-loader
- workerize-loader
- worklet-loader
- nativescript-worker-loader
- thread-loader
- svg-sprite-generation-loader

This doesn't by itself fix any of them fully, but does increase
compatibility
2026-09-09 12:46:24 -07:00
Niklas Mischkulnig 3c8e9c3855 Support durable use-cache entries with client components (#98140)
The current setup is:
1. A use-cache function returns an RSC value that may contain client
reference proxies. Flight serializes each reference using the client
module `id`, export `name`, client `chunk` list, and `async`. The
resulting Flight payload is stored in the cache entry.
2. When decoding the cache entry, the client module `id` is resolved
through `rscModuleMapping` to obtain the corresponding RSC module `id`
and `export`.

Cache decoding does not need the client chunk list because it does not
load client chunks. The outer RSC serialization emits the client
reference again using the current client reference manifest, including
the current chunks. We can therefore serialize cached references with an
empty chunk list.

The client module `id` is also unstable across builds. Instead, cache
entries can store the stable client reference name. When decoding, we
resolve: client reference name -> current client module ID -> current
RSC module ID
This makes cached client references independent of build-specific module
IDs and chunk filenames.
2026-09-09 14:42:15 +02:00
Tobias Koppers 73a26bfc84 feat(next-napi-bindings): build Turbopack for wasm (#97586)
### What?

Builds Turbopack for wasm, so the wasm fallback can support Turbopack
and not just SWC.

### Why?

`next-napi-bindings` gated every Turbopack dependency behind
`cfg(not(target_arch = "wasm32"))`, so the
wasm fallback shipped SWC only. That is the reason issues like #96960
have no answer today: when the
native binding cannot load there is no Turbopack at all.

### How?

Moves the Turbopack dependencies into the shared dependency block,
leaving genuinely native-only crates
gated:

- `lightningcss` / `turbopack-lightningcss-napi` and
`swc_ecma_react_compiler` back the `css` and
  `react_compiler` modules;
- `swc_plugin_backend_wasmtime` cannot run inside wasm;
- SWC's plugin-host features, gated in the previous layer;
- `tokio`'s `full` feature pulls in `process` and `signal`, which do not
exist on wasi, so the wasi build
  gets its own `tokio`.

This is the layer at which `swc` first gets compiled for wasm, which is
why the plugin-host gating has
to land **before** it: with that in place, `cargo check -p
next-napi-bindings --target
wasm32-wasip1-threads` passes here with no `[patch.crates-io]` override.

### What this does *not* do

It makes Turbopack **compile** for wasm. It does not make it run: still
outstanding are the
`env.read_custom_section` loader hook, a wasm `module_init` for the
tokio runtime and panic hook, a
host-provided fetch, the persistent cache (WASI preview1 has no
file-backed mmap or flock), and file
watching. Shipping is separately blocked on the unreleased emnapi v2.

`turbo-tasks-malloc`'s `custom_allocator` feature also stays off, so
there is no per-thread allocation
accounting on wasm and memory figures are absent from traces — an
observability gap, not a correctness
one.


<!-- fleet b6d0486f-97c7-42a7-bdaf-3490774cdec3 -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-09 10:25:45 +02:00
Tobias Koppers 49dc4c84b4 Turbopack: fix output tracing include glob traversal (#98382)
### What?

Make Turbopack's `outputFileTracingIncludes` file patterns root-relative
and targeted during output tracing. Reject unanchored partial globs when
code attempts directory-prefix matching. Add production coverage using a
real pnpm-installed WASM package, plus unit coverage for anchored
directory pruning and include-pattern expansion.

### Why?

Turbopack previously treated include file patterns as unanchored partial
matches. The directory-prefix matcher could therefore accept every
descendant after matching the first literal segment: an exact WASM
include under `node_modules/.pnpm` caused 2,093 `read_glob_inner`
executions instead of traversing only its 5 directory prefixes.

Unanchored patterns also cannot soundly answer whether a directory may
contain a future match, so allowing `contains: true` with
directory-prefix matching risks both over-traversal and missed files.
Include behavior also differed from webpack and the documented
project-root-relative semantics.

### How?

Build anchored alternatives for each include pattern: one for the direct
match and one for descendants of a matched directory. Anchoring lets
`read_glob` prune unrelated directories, while the recursive alternative
preserves existing behavior for matched directories and directory
symlinks, including their resolved target contents.

`can_match_in_directory` now fails immediately for `contains: true`,
while direct partial matching remains supported. Route-key and exclusion
matching continue to use direct partial matches and are unchanged.

The production fixture resolves
`lightningcss-wasm/lightningcss_node.wasm` from a real
`lightningcss-wasm@1.28.2` dependency, avoiding assumptions about pnpm's
store naming or patch hashes.

### Verification

- `CARGO_INCREMENTAL=0 pnpm build-all`
- `cargo fmt -p turbo-tasks-fs -p next-api -- --check`
- `cargo test -p turbo-tasks-fs glob` (84 passed)
- `cargo test -p next-api` (17 passed)
- `pnpm test-start-turbo
test/production/app-dir/output-file-tracing-includes-read-glob/output-file-tracing-includes-read-glob.test.ts
test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts
test/production/build-trace-extra-entries-monorepo/build-trace-extra-entries-monorepo.test.ts`
(3 passed)
- `pnpm test-start-webpack
test/production/app-dir/output-file-tracing-includes-read-glob/output-file-tracing-includes-read-glob.test.ts`
- The shared fixture asserts that both bundlers include a root-relative
exact match and exclude a nested file with the same suffix
- Measured the exact reproduction at 5 include-specific
`read_glob_inner` executions, down from 2,093

<!-- NEXT_JS_LLM -->


<!-- fleet b3cefb44-e1ce-4bd6-83a9-da2f22848cad -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-09 09:39:19 +02:00
Luke Sandberg 97e7d901c8 turbo-tasks: explicit GC root anchoring + cross-session orphan reclamation (#96857)
### What?

Reworks how the reference-counting GC decides what is a **root** (a task
that must not be collected), and adds a durable path to reclaim
cross-session orphaned roots

Currently GC roots are persisted and the only way to get collected is
for another session to revive them, and then for their ref-counts to
drop. This provides a new mechanism.

1. **In-session** — Top level operations get reference counted so they
can be dropped in session.
2. **Cross-session** — live roots are recorded along with a simple aging
mechanism so they can still be collected even if a later session never
restores it.

### Why?
This closes the final GC gap. Top level operations are ambiguous
currently and need tracking. With cross session aging we close a basic
gap where if you delete a route between sessions we never clean up the
tasks. Now it will eventually age out.

### How?

**Roots are anchored explicitly**. Via pins and RAII guards like
`GcRoot`

**Persisted roots set + TTL age-out** A new Infra-keyspace `GcRoots`
entry persists `Vec<(TaskId, ttl)>`. That allows us to detect when roots
are stale and can be dropped.

**Dispose transient root tasks on drop.** `RootTask::Drop` now disposes
tasks and cleans up references allowing the GC to work
2026-09-08 09:03:09 -07:00
Jimmy Miller 8b9cdc5eef perf(turbopack): skip ignored files during server tracing (#97475)
So I was playing around with profiling some very simple next apps when I
noticed this. Basically, because of how things were set up we were
spending a good chunk of our build time analyzing files we were going to
reject. I went around and around trying different combinations to make
sure we ignore those files (and only the files we should ignore) to
speed up build times. Since it is a constant impact, mostly on small
projects, but on some, it can make a big difference. I had some
adversarial rounds with agents to see if they can break the config in
the ways the old comments hints at why we didn't do that. And this final
round finally couldn't get it to break.

For a v0-generated Hello World app, compile time is 28.0% faster
(+/-1.7%) and a full clean build is 15.9% faster (+/- 1.2%).

---------

Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-09-03 16:16:54 +00:00
Niklas Mischkulnig 538f3f68e9 Bump swc to v77 (#97407)
There have been breaking changes to the AST:
- `BlockStmtOrExpr` is now `ArrowFunctionBody`
- function bodies are `FunctionBody` instead of `BlockStmt`
- JSX text literals are `Wtf8Atom`, instead of `Atom`
2026-09-02 16:09:18 +02:00
Will Binns-Smith ec107c16dd lazy server hmr (#96566)
## Summary

Make Turbopack server HMR demand-driven. Server updates are now compiled
and applied when the next relevant request writes an endpoint, instead
of eagerly evaluating changed server modules after every file change.

This replaces the aggregate server HMR subscription with an on-demand
update API, while preserving incremental updates and falling back to
full cache eviction when a restart is required.

Test Plan: added an e2e test

<!-- NEXT_JS_LLM -->

---------

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>
2026-08-31 09:12:51 -07:00
Niklas Mischkulnig 7a15847525 Turbopack: add next_config.use_react_experimental getter (#98032)
Refactor to unify the multiple places where this computation was
happening
2026-08-28 10:33:43 -07:00
Niklas Mischkulnig e3790aa911 More granular cache keys for use-cache entries (#95233)
Caveats:
- I had to skip `react[-dom][/*]` and
`private-next-rsc-server-reference` and `private-next-rsc-cache-wrapper`
imports in the code hash and env-var tracking. Because those all end up
pulling in app-page-turbo.runtime.prod.js which reads many env vars and
would cause constant deopting. But this should still be correct. The
code of these imports is included via the Next.js version, and no env
vars should change the semantics of any of those imports.
- Static env var reads are collected, but too dynamic accesses are
silently ignored and don't lead to deopts (same goes for env var reads
in native NAPI addons). This means that this cache reuse is not
guaranteed to be 100% guaranteed to never lead to stale caches.
- Code hashing and env var collection happens on a per-module basis. So
if you put all use-cache functions into a single file and/or together
with react components, then you will see extraneous invalidations. This
will be fixed by either generally enabling module splitting for all of
Turbopack, or by adding a special transform that does it for use-cache
functions.

Followups:
- There are some env var static analysis gaps that will be immediate
followups before broader testing. These are the various TODOs added in
https://github.com/vercel/next.js/pull/95310
- Client components invalidation is very coarse grained right now
(statically imports any client component anywhere -> deopt completely).
Followup for the future. But the current setup is correct. This would
just improve effectiveness further
- Do this in dev as well. Currently there is no NFT at all in dev (for
performance reasons)
- Module splitting for more granular tracking
- Include entropy when serializing server reference arguments for cache
key


Todo:

- [x] Use implementation code hash (includes inlined env vars): from
#94234
- [x] Include non-inlined runtime env vars: from #95310
- [x] Include client reference manifest (very coarse for now)
- [x] Include Next.js version (for wire format, etc)
- [x] This is now done for all use-cache entries now. Not just for `use
cache: remote`. Is that the intended behavior? Yes
- [ ] ~~if `NEXT_DEPLOYMENT_ID` is in the env vars. just deopt and don't
care about stringifing and hashing the env vars~~
- [x] Is cache key size a problem? Currently you can get this: (values
are always hashed)
`CustomCacheHandler::get
["80e6f6560092f0078775e7e787c1c10ecf6dea0bc4",[],["d984fbaa996274737f3b59345a300a20","16.4.0-canary.5","__NEXT_NO_MIDDLEWARE_URL_NORMALIZE=undefined","NEXT_OTEL_PERFORMANCE_PREFIX=undefined","__NEXT_PRIVATE_ORIGIN=a04f4b9d6a8f42724740a480e9a2bc67c053dc04377be831c0e2c407a1422004","NEXT_PRIVATE_RESPONSE_CACHE_TTL=undefined","NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE=undefined","__NEXT_CACHE_COMPONENTS=b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b","__NEXT_ROUTER_BASEPATH=undefined","__NEXT_MANUAL_CLIENT_BASE_PATH=undefined","__NEXT_INSTRUMENTATION_CLIENT_ROUTER_TRANSITION_EVENTS=undefined","__NEXT_APP_NAV_FAIL_HANDLING=undefined","__NEXT_GESTURE_TRANSITION=undefined","__NEXT_USE_OFFLINE=undefined","NEXT_DEBUG_BUILD=undefined","__NEXT_VERBOSE_LOGGING=undefined...]]
[["_N_T_/layout","_N_T_/page","_N_T_/","_N_T_/index"]]`
2026-08-28 10:17:15 -07:00
Tobias Koppers 17a901a74f Guard filesystem reads against unresolved symlinks (#97902)
### What?

Adds debug-only OS realpath validation to successful `DiskFileSystem`
file and directory reads. When a successfully canonicalized path differs
from the supplied path, the read returns a normal task error naming both
paths.

Fixes pattern/glob traversal and NFT tracing so physical filesystem
access uses resolved paths while logical paths remain available for
user-visible specifiers and complete symlink-chain recovery.

### Why?

Reading through an unresolved symlink parent gives the same filesystem
object multiple path identities. That can make Turbo Tasks dependency
tracking and invalidation inconsistent and can produce invalid
deployment ZIPs when NFT output contains files below unresolved links.

The checks return errors rather than asserting because paths can
disagree temporarily under eventual consistency. Propagating a task
error avoids panicking a worker thread while still exposing invalid
callers during development.

### How?

The validation lives directly in `DiskFileSystem::read` and
`DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API
inline instead of the Turbo Tasks realpath task, keeping the diagnostic
out of the task dependency graph. The guard runs only after the OS read
succeeds, so missing/non-directory probes preserve their existing
behavior.

`read_matches` resolves each physical directory immediately before
enumeration while retaining logical `PatternMatch` paths.

`read_glob` and `track_glob` now resolve their initial directory before
enumeration. Symlinks discovered later through wildcard segments are
also traversed through resolved targets. `ReadGlobResult` deliberately
retains logical paths rooted at the supplied base, allowing consumers to
call `realpath_with_links` and recover the complete symlink chain.

Consumers follow that contract explicitly:

- NFT includes expand each logical match with `realpath_with_links`,
emit resolved files and every traversed symlink, skip resolved directory
targets, and deterministically deduplicate/sort output.
- `import.meta.glob` uses recursive logical keys as the source of
user-visible requests, while module resolution follows and tracks
symlinks.
- the hash-glob example resolves returned logical paths before reading.

Webpack-loader context dependencies are covered for both
`path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader
fixture performs its directory read with Node `fs`, reports the
directory using `addContextDependency`, and Turbopack tracks the
resolved target.

### Verification

- `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --
--check`
- `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api
--all-targets`
- `cargo test -p turbo-tasks-fs` (128 passed)
- `cargo test -p next-api` (7 passed)
- `cargo check -p turbo-tasks-fs --examples`
- `import.meta.glob` symlink execution fixture (1 passed)
- Nine targeted node-file-trace CI cases with `release-with-assertions`
(9 passed)
- `pnpm build-all`
- `webpack-loader-fs` Turbopack dev e2e (1 passed)
- `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed)
- twoslash Turbopack production, normal mode (4 passed)
- twoslash Turbopack production, cache-components mode (4 passed)
- `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200)

### Notes

The disk guard is cross-platform, while its symlink-parent regression
test is Unix-only, matching neighbouring symlink tests. On Windows, OS
canonicalization can also normalize casing and 8.3 short names; a debug
read using a non-canonical spelling will therefore return the same
diagnostic error.

<!-- NEXT_JS_LLM -->


<!-- fleet 1d32e12c-f4ec-4f22-862a-c85f0005805c -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-28 08:31:06 +02:00
Tobias Koppers 6f44fd34a1 Turbopack: enable export mangling by default in production builds (#97676)
Stacked on #97672 — enable export mangling by default on canary
releases.

### What?

Two defaults change:

- **Next.js builds.** `experimental.turbopackMangleExportNames` is
pinned to `false` for stable
releases and left *unset* on canary, where Turbopack then defaults it
from the build mode: on for
production builds, off in development. Explicitly setting the option
wins in either direction, so
  setting it to `true` in development is honoured.
- **Turbopack's own execution test suite.**
`TestOptions::mangle_export_names` defaults to `true`,
so all 279 fixtures exercise mangling instead of only the handful that
opt in.

### Why?

The feature is verified by turbopack's own fixtures, targeted e2e
suites, and a couple of
bundle-size measurements — a narrow slice of what Next.js's test suites
actually cover. Defaulting
it on for canary puts every production-mode e2e and integration test
through the mangled code path
for real users of the canary channel, without committing stable users to
it yet. Turning it on for
stable is a separate, later decision that can stack on top of this once
canary has soaked it.

Broad exposure has already earned its keep several times over. Turning
mangling on in CI and in the
fixture suite surfaced bugs that no hand-picked suite had found,
including a module-fragments helper
handing out another module's exports value (#97672's
`EcmascriptExports::borrowed()`), a
client-reference proxy memoized per wrapper instance rather than per
content, and a
code-elimination bug for export-less modules (fixed in the follow-up
PR).

### How?

`defaultConfig` cannot see the build mode, so it only expresses the
stable/canary split
(`isStableBuild() ? false : undefined`) and
`NextConfig::turbopack_mangle_export_names(mode)`
supplies the mode-dependent default on the Turbopack side. This is
deliberately *not* the shape of
the neighbouring `turbopackSharedRuntime: !isStableBuild()`: mangling
should not apply in
development, and hard-forcing `false` there in Rust — as an earlier
revision of this PR did — would
silently ignore a user who asked for it explicitly.

Mangling does **not** depend on minification. An earlier version of this
PR gated it on
`minify(mode)`, which was wrong and has been removed; `--no-mangling` is
a minifier flag and does
not affect export mangling.

Verified with a real `next build` A/B on a small app: the option left
unset mangles (959,174 B total
emitted JS, 293,407 B gzipped) while explicitly setting it to `false`
does not (966,519 B / 294,963
B gzipped), confirming both the default and the override. All 279
execution fixtures pass with the
suite default flipped, with no fixture needing an opt-out.

Note this layer does not change the `turbopack-emit-collect` snapshots
on its own: making a module
without re-exports split additionally requires the split trigger added
in the follow-up PR, so those
snapshots move there.

This is the layer to revert if canary turns up problems specific to
running with the option on by
default; #97672 remains useful as an opt-in feature either way.

<!-- NEXT_JS_LLM -->


Co-authored-by: Luke Sandberg
<210140+lukesandberg@users.noreply.github.com>


<!-- fleet ecdfa248-cd54-41ac-b4a2-c9d49e2a67ee -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
2026-08-28 07:46:17 +02:00
Tobias Koppers a84bc8de2a Turbopack: mangle exported names for smaller bundle sizes (#97672)
### What?

Adds export-name mangling to Turbopack, behind a new experimental option
`experimental.turbopackMangleExportNames` (default `false`). It is
independent of minification:
`--no-mangling` is a minifier flag and does not affect it.

When enabled, each ECMAScript module's *used* export names — including
`default` and
`__esModule` — are replaced by short keys in the emitted output, both
where the module registers
its exports and where every consumer reads them. Modules whose export
names could be observed by
user code keep their original names, decided per module.

This is a reland of #89060 (on top of the already-merged #89406),
originally written by Matt
Mastracci, who is credited as a co-author on the commit.

Stacked on top: #97676 flips the default to `true` on canary releases,
so Next.js's own CI
exercises the feature broadly before it is considered for stable.

### Why?

Bundle size. A module's export keys exist only to link modules together:
the producing module
emits `{ someVeryLongExportName: … }` and every consumer reads
`ns["someVeryLongExportName"]`.
Both sides are generated by us, so as long as producer and consumer
agree — and the name isn't
observable from user code — the key can be a single character. Long
export names are extremely
common in real dependency graphs (icon sets, utility packages, barrel
files), and each one is paid
for once in the module that defines it and once per importing module.

### How?

**Ported, not rebased.** `canary` is ~2500 commits past the original
stack's base, and the files it
touched were independently rewritten in the meantime (export-analysis
refactor #92781, CJS analysis
for scope hoisting #95826, the `module_fragments` subsystem #95978). A
probe rebase produced 16
conflicting files on the first commit alone, so the original branches
were used as a reference
implementation — for intent, the identifier alphabet, and test coverage
— and the feature was
rebuilt on today's infrastructure. #89561 from the original stack
(erasing the Next.js wrapper
module types) is deliberately **not** part of this change; it turned out
to be unnecessary, because
those modules already declare whole-module export usage and therefore
back off on their own.

**The name table** (`references/esm/mangle/table.rs`) hashes each name
into a table of all valid JS
identifiers of the smallest length that fits the name set — 15 exports
get single-character keys —
and resolves collisions by open addressing. Hashing rather than
assigning `a`, `b`, `c`, … is what
keeps names stable: an unrelated edit elsewhere in the module doesn't
renumber every other export,
and a collision only perturbs its own cluster. Assignment happens in two
passes: every name that is
*already* a valid identifier at the chosen length keeps itself and
reserves its bucket first, and
only then is anything hashed — so an export called `a` keeps `a`, and
nothing else can be assigned
it. Both passes iterate in sorted order, so the mapping depends only on
the set of names.

A module with exactly **one** mangleable export is special-cased to a
fixed key, `f`, rather than a
hashed one. `f` is the most common character in JS keywords (`if`,
`for`, `function`), and every
single-export module in the graph then emits the same `.f` / `.f()` byte
sequences, which gzip's
back-references pick up across the whole bundle — a bigger win than
hashing, at the cost of that one
key changing when a second export is added.

A fixed list, `RESERVED_KEYS`, is withheld from every table for two
different reasons: JS reserved
words (`if`, `in`, `do`, `for`, `let`, `new`, `try`, `var`) are legal as
quoted property keys but a
minifier will not fold `ns["if"]` into the shorter `ns.if`, so handing
one out costs bytes instead of
saving them; and `__esModule` is withheld because the runtime's `esm()`
helper defines that property
on every module's exports object regardless of what the module itself
exports, so an assigned key
landing on it would collide. (`default` needs no such protection — once
it is mangled like any other
export, nothing else emits a property under that literal name.)

**One source of truth for the mapping.** `mangled_export_names(module,
chunking_context)` is a
turbo-task that both the producing side (`EsmExports::code_generation`)
and the consuming side
(`ReferencedAssetIdent::Module`, the single place a cross-module export
access is materialized) ask
for the *target* module's map. Neither side computes a table of its own,
so they cannot disagree,
and the task derives export usage from the chunking context itself
rather than accepting it as an
argument, so a caller can't supply usage from the wrong graph. Re-export
chains need no special
handling, because the consumer side already resolves through re-exports
to the module that produces
the binding.

The mangling decision itself lives on `EsmExports` as a
`mangle_export_names: bool` field, rather
than a separate trait method every module type has to override. A module
that derives its exports
from another one (a facade, a locals module, a part, a rename) inherits
the flag with the data,
which removed seven hand-written delegations and the possibility of a
new wrapper type forgetting
one.

**A mangling decision must not cross module identities.** A few module
types hand out *another*
module's exports value as their own (the WASM loader module, the
module-fragments side-effects
wrapper, the client-reference proxy). If that borrowed value carried a
real mangling decision, the
producing and consuming sides would key their lookups on two different
modules and could compute two
different keys for the same export — this actually broke every WASM- and
`@vercel/og`-based test
once the default-on layer exercised it in CI.
`EcmascriptExports::borrowed()` is the one place this
is handled: it always returns an unmangled view, and every such
pass-through site uses it.

**Back-off is per module**, built on the export-usage information that
landed after the original PR
(`BindingUsageInfo` / `ModuleExportUsageInfo`) rather than the
original's locals/facade-split
heuristic. A module keeps its names when its usage is `All` (a namespace
import that couldn't be
lowered, a computed property access, an unresolvable `export *`, or a
chunk-group entry — which
covers client references and the Next.js wrapper modules), when it is
read through a namespace value
at all, when its exports are dynamic or not statically known ESM, or
when names aren't being mangled
in this build.

`__webpack_exports_info__` gains `canMangle` and `mangledName` per
export, which is how a running
test can observe the mapping; with the option off it emits exactly what
it emitted before.

### Testing

- `turbo-tasks-hash`/table unit tests: encode/decode round-trip,
degenerate-name rejection, table
sizing, the single-export fixed key (including its own reservation),
reserved-word withholding
(including a reserved bucket-count test that stays in sync with the
reserved list), the
preserved-name pass running before any hashing, uniqueness under heavy
collision, order
  independence, wrap-around probing, and same-tier stability.
- 13 `turbopack-tests` execution fixtures under
`tests/execution/turbopack/exports/mangle-*`,
several ported from the original PR and from webpack's
`test/configCases/mangle`: named imports,
re-export chains and default exports (including one literally named
`__esModule`), escaping
namespaces (`Object.keys`, `delete ns.missing`, `export * as`, CJS
interop), destructuring,
prototype-shadowing names (`toString`, `$1`, `__1`), a 60-export
two-character table, dynamic
`import()` with `webpackExports` / `turbopackExports`, a CommonJS
consumer of an ESM module,
dynamic re-exports, scope hoisting on and off, and a control with the
option off.
- 3 committed snapshot fixtures under `tests/snapshot/mangle-exports`,
so the emitted keys, the
  back-off, and the fixed single-export key are visible in review.
- One fixture under `__skipped__`, which the harness asserts *fails*,
recording the
  namespace-materialization gap below.
- Full suite: 546 unit + 272 execution + 125 snapshot tests pass, with
no snapshot churn across the
  several refactors this PR went through in review.
- Verified against real builds: targeted app-dir, worker, WASM, and
`@vercel/og`-based e2e suites
pass with the option forced on (the failures that remain are
external-network tests that fail
identically with it off), and a small two-page app shrank by 0.76% of
total emitted JS / 0.53%
  gzipped.

### Known limitations, each intentional

- **A module read through `import * as ns` is never mangled**, even when
every read is statically
tracked, because the analysis doesn't yet distinguish a lowered named
read from a materialized
namespace object. Namespace imports are common, so this leaves real wins
on the table; unlocking
  it is the highest-value follow-up.
- **Escaping namespaces back off entirely.** Webpack instead keeps
mangling and materializes a
namespace object keyed by the original names. That is the `__skipped__`
fixture: implementing it
  turns the suite red until the fixture is moved out.
- **CommonJS export mangling is out of scope.** The producing side is
reachable, but the consuming
accesses live in user source and nothing rewrites them today; it needs
its own design pass.

Closes PACK-435

<!-- NEXT_JS_LLM -->


Co-authored-by: Luke Sandberg
<210140+lukesandberg@users.noreply.github.com>


<!-- fleet ecdfa248-cd54-41ac-b4a2-c9d49e2a67ee -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
2026-08-28 07:46:16 +02:00
Niklas Mischkulnig 1f52cb42ad Turbopack: shorten CSS module class names (#97944)
The class names were unnecessarily long.

Use the same as the lightningcss default:
`[hash]_[local]` which is
`<hash of the full file path>_<original class name or identifier>`

Keep the previous longer mechanism to aid in debugging in dev

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 21:22:53 +00:00
Josh Story 6d6228c0c6 Prune incomplete parallel route matchers (#97108)
## Summary

This adds an experimental `strictRouteMatching` flag that leaves a
matcher out when its finalized loader tree contains a synthesized
default that will always call `notFound()` for a slot actually declared
by the owning layout. Adding a `default.tsx` keeps the matcher, and
structural router-state branches that are not real slots of that layout
do not make it incomplete. The flag defaults to `false` so matcher
pruning can roll out independently from the preceding loader-tree
correction.

The main goal is to make `children` and named slots behave the same way.
If a URL cannot construct a complete route tree on its own, it should be
treated as an unmatched URL instead of sometimes showing a slot 404 and
sometimes producing a missing page or default error.

## Semantics

For example:

```text
app/split-matcher/
├── layout.tsx
├── foo/page.tsx
├── bar/page.tsx
└── @slot/[...parts]/page.tsx
```

With strict route matching disabled, Next.js emits matchers for
`/split-matcher/foo`, `/split-matcher/bar`, and the broad
`/split-matcher/[...parts]`. The broad matcher can match any URL, but
its `children` branch has neither a matching page nor a default, so it
can only construct a permanent 404 tree. With strict route matching
enabled, that broad matcher is omitted while `/foo` and `/bar` remain
because each combines a real `children` page with the named-slot
catchall. Adding `app/split-matcher/default.tsx` deliberately makes the
broad matcher complete and keeps it.

The same rule applies without a catchall:

```text
app/disagreeing-slots/
├── layout.tsx
├── @first/foo/page.tsx
└── @second/bar/page.tsx
```

`/foo` is incomplete because `@second` has no matching page or default,
and `/bar` is incomplete for the corresponding reason in `@first`, so
neither matcher is emitted. A later PR in this stack reports the
now-unreachable page files as a project misconfiguration.

A declared `children` route is treated like any named slot:

```text
app/declared-children/
├── layout.tsx
├── page.tsx
└── @panel/
    ├── default.tsx
    └── details/page.tsx
```

`/declared-children` is complete because `children` uses `page.tsx` and
`@panel` uses its default. `/declared-children/details` is incomplete
because `@panel` matches its page while the declared `children` slot has
no matching page or default at `/details`, so that matcher is pruned.

A route composed entirely from declared named slots is also complete:

```text
app/named-only/
├── layout.tsx
├── @left/[...slug]/page.tsx
└── @right/[...slug]/page.tsx
```

The preceding PR's default-on `explicitParallelRouteChildren` behavior
means this loader tree contains only `left` and `right`. Strict matching
keeps `/named-only/[...slug]` because both declared slots match; it does
not invent `children` and then prune the route for failing to satisfy
that nonexistent slot.

We got here incrementally.
[#47872](https://github.com/vercel/next.js/pull/47872) introduced the
404 fallback for unmatched parallel slots,
[#60186](https://github.com/vercel/next.js/pull/60186) added a
development warning because this was confusing in practice and linked
[#51805](https://github.com/vercel/next.js/issues/51805) and
[#49569](https://github.com/vercel/next.js/issues/49569), and
[#84702](https://github.com/vercel/next.js/pull/84702) later made a
missing default a build error for named slots while leaving `children`
on the old fallback for backwards compatibility. Strict matching takes
the next step and treats a matcher that can only construct a permanent
404 tree as unmatched.

This changes soft navigations that only worked by preserving a
previously active slot even though the URL could not be loaded directly,
which is why matcher pruning remains behind its own experimental flag.
The interception retention markers backed by `default-null`, including
named host slots from the first PR in this stack, are complete route
patches and are not pruned. This change however is well motivated
because if you did client nav to a route that only matches a named slot
and then hard refresh you will end up getting a 404. This is a sign our
current semantics are actually broken.

If you want to preserve the perma 404 behavior of the
slot-without-default you can just add a default and make it call
`notFound()` unconditionally.

## Verification

- `pnpm build-all`
- Focused `normalize-catchall-routes` unit coverage
- Production and development e2e coverage on Turbopack and Webpack
- The pruning e2e matrix with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 13:23:43 -07:00
Josh Story 05ed7c1be2 Omit undeclared children slots from app routes (#97184)
## Summary

Parallel route layouts can be composed entirely from named slots, but
loader tree construction currently synthesizes a `children` fallback
whenever any named slot exists. This makes `children` semantically
required even when no page, default, or ordinary route branch declares
it.

This adds `experimental.explicitParallelRouteChildren` and enables it by
default. When enabled, `children` is included in a layout slot set only
when the filesystem declares an ordinary route that can render at that
level. A layout by itself is only structure and does not declare a route
target. Ordinary descendants are traced until they reach a page or
default, including through deeper named slots, before they cause
`children` to be included. Setting the flag to `false` temporarily
restores the legacy implicit `children` fallback.

This flag only controls whether `children` exists in the loader tree. It
does not prune incomplete matchers; that is the separate
`experimental.strictRouteMatching` behavior in the next PR.

Named slots keep their existing default and soft navigation semantics.
The preceding PR retains the slots owned by an interception host without
treating `children` specially, so an undeclared child is no longer
needed for that behavior. A real `children` branch still uses the
retention marker when it is one of the host layout slots.

## Semantics

For example, this layout declares only named slots:

```text
app/dashboard/layout.tsx
app/dashboard/@left/page.tsx
app/dashboard/@right/page.tsx
```

With `explicitParallelRouteChildren` disabled, Next.js adds a synthetic
`children` branch whose built-in default calls `notFound()`, even though
the layout never declared or rendered it. With the default behavior
enabled, the loader tree contains only `left` and `right`, so
`/dashboard` is matched from the route targets that actually exist.

This is different from an ordinary branch whose route targets are deeper
in the tree:

```text
app/nested/layout.tsx
app/nested/@sidebar/[...slug]/page.tsx
app/nested/content/layout.tsx
app/nested/content/@left/[...slug]/page.tsx
app/nested/content/@right/[...slug]/page.tsx
```

Here `content` really is the `children` branch of `nested`. The scan
follows `content` through its layout and deeper named slots, so
`children` remains required. `/nested/content/anything` can construct
every declared slot, while `/nested/incomplete` only matches `sidebar`
and is still incomplete. The distinction is whether the ordinary
descendant eventually reaches a page or default, not whether a layout
happens to exist along the way.

The focused children detection coverage proves that a layout-only
descendant does not synthesize `children`, while an ordinary branch
whose route targets live inside deeper named slots still does. The
limitation coverage also proves that named-only trees render pages, CSS,
metadata, and regular error boundaries. It intentionally asserts the
current broken behavior for HTTP access fallbacks and metadata or
viewport failures so those expectations can be flipped when renderer
ownership no longer depends on `children`.

## Verification

- `pnpm build-all`
- Turbopack and webpack development and production coverage for
`explicit-parallel-route-children-detection`
- Turbopack and webpack production coverage for
`interception-dynamic-segment`, `parallel-routes-layouts`, and
`explicit-parallel-route-children-legacy`
- The same existing production coverage with Cache Components enabled
- Turbopack and webpack production coverage for the documented
named-only limitations

<!-- NEXT_JS_LLM -->
2026-08-27 12:22:31 -07:00
Josh Story cce7bf2fc0 Retain interception route host slots (#97242)
## Summary

Interception routes represent a partial update to the layout that hosts
them. Today we model this for `children` by synthesizing a `__DEFAULT__`
route backed by `default-null`, but named siblings still use normal
default matching even though they should retain their active state too.

This makes retention relative to the interception host instead of the
`children` key. Every non-intercepting sibling gets the existing
`__DEFAULT__` marker backed by `default-null`, while slots inside the
newly selected interception subtree continue to use normal matching and
real defaults.

The coverage uses a named-only host to verify that named siblings retain
client state without evaluating a user default, that a sibling without a
default also retains, and that a hard refresh still loads the canonical
route.

## Example

Consider this route tree:

```text
app/
├── named-host/
│   ├── layout.tsx                  # renders canonical, content, secondary, and modal
│   ├── @canonical/
│   │   └── page.tsx
│   ├── @content/
│   │   ├── page.tsx                # renders a stateful counter
│   │   └── default.tsx             # throws if evaluated
│   ├── @secondary/
│   │   └── page.tsx                # has no default.tsx
│   └── @modal/
│       ├── default.tsx
│       └── (..)named-target/
│           └── page.tsx            # intercepted target
└── named-target/
    └── page.tsx                    # canonical route
```

Before this change, a soft navigation from `/named-host` to
`/named-target` could replace `@content` with its throwing default and
treat `@secondary` as missing because only `children` had retain
semantics. After this change, only `@modal` switches to the intercepted
page; `@content` and `@secondary` keep their existing UI and the counter
keeps its state. A hard refresh still renders the canonical
`/named-target` route instead of the interception host.

## Verification

- Turbopack and webpack production coverage in
`interception-dynamic-segment`
- The same production coverage with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 09:27:33 -07:00
Niklas Mischkulnig 286fcc346f Turbopack: call loadActionManifest for app-route (#97921)
For #95233, I need to read server-reference-manifest.json even for App
Routes, which previously didn't emit server action manifest entries at
all (because thus far, it was unnecessary).
2026-08-27 10:14:32 +02:00
Niklas Mischkulnig ae622d6ce9 Migrate from box_patterns to deref_patterns (#97924)
TLDR: just enable `deref_patterns` and remove the `box` syntax. And it
just works

`box_patterns` will be removed entirely in the next Rust nightly:
https://github.com/rust-lang/rust/pull/156749
2026-08-26 14:01:02 +00:00
Tobias Koppers 43d81b9d55 feat(turbopack): resolve /-rooted imports from the project directory (#97799)
### What?

A request starting with `/` — `import '/content/where'`,
`require('/foo.js')` — now resolves from
the **project directory** (the one holding `next.config`), and cannot
reach outside it. Previously
it resolved from the root of the filesystem while reporting *"server
relative imports are not
implemented yet"*.

This is the base of a stack: the follow-up on top of it moves
`import.meta.glob`'s `/`-rooted
patterns onto the same root, so a pattern and a plain import agree on
what `/` means.

### Why?

The feature was half-present and mislabelled. The
`Request::ServerRelative` arm rewrote `/foo` to
`./foo` and resolved it against the filesystem root, then emitted a "not
implemented yet" issue
unless an import-map alias had already matched. Users got a diagnostic
telling them to give up on
something that in fact resolved.

Worse, resolving from the *filesystem* root means `/` escapes the
project. In a Turbopack execution
test — where the filesystem root is the repository root —
`require('/package.json')` resolved this
repository's own `package.json`. In a monorepo, where Turbopack's root
is the workspace root and the
app lives in `apps/web`, `/content/x.js` would reach a workspace-level
file rather than the app's.

The intended semantics are Vite's, and they were confirmed by
measurement rather than from memory.
In a workspace where `content/*.js` exists *both* at the workspace root
and under the Vite root, so
the two are distinguishable, Vite 5.4.21:

- resolves `/content/…` from the configured `root` — changing `root`
changes which file wins;
- fails outright for a file that exists only above `root`, rather than
walking up;
- behaves identically for a plain `import` and for `import.meta.glob`.

### How?

`ResolveOptions` gains `server_relative_root`, the directory a
`/`-rooted request resolves from, and
`ResolveOptionsContext` exposes it so embedders can set it. Next.js sets
it to the project directory
in the client, server and edge resolve contexts — all of which already
had it to hand, so nothing new
is threaded through.

Three deliberate choices:

- **The option is optional and defaults to the previous behaviour.**
`turbopack-core` is used well
beyond Next.js; an embedder that doesn't set it keeps resolving `/` from
the filesystem root. This
is also why the change is provably contained — the whole
`turbopack-tests` suite passes with no
  snapshot churn.
- **It is named and documented in filesystem terms**, tied to the
existing `Request::ServerRelative`
vocabulary, rather than introducing a "project root" concept into
`turbopack-core`, which must stay
  free of Next.js concepts.
- **There is no fallback.** Resolution happens in the configured root
and stops; a request that isn't
there is a normal module-not-found. A fallback to the filesystem root
would reintroduce exactly the
  escape this fixes.

The "not implemented yet" issue is removed. The equivalent issue for
Windows-style absolute requests
is untouched, as is the separate tsconfig `extends` handling, which
treats a rooted path as absolute
per TypeScript's own rule.

### Testing

The Turbopack execution harness already has two distinct roots — the
repository root as the
filesystem root, and each test's own directory — so pointing the new
option at the test directory
makes "project directory" and "filesystem root" genuinely different, and
the distinction testable
without a Next.js app. Two fixtures: one where a `/`-rooted import and
require resolve from the
project directory, and one asserting that `/package.json`, which exists
only at the filesystem root,
does *not* resolve. The former emits no issues at all, which is what
pins the removal of the
diagnostic.

The end-to-end case uses the existing monorepo fixture, where the
project directory really is not the
filesystem root. A file of the same name exists in both
`apps/web/content` and the workspace root, so
the rendered value alone identifies which root was used — a test that
merely asserted "it renders"
would have passed under the old behaviour too.

### Known gap

TypeScript resolves a leading `/` as an absolute path on disk, so a
`/`-rooted import in a `.ts` file
still fails type checking (`TS2307`) even though it bundles and runs.
The e2e page is therefore
JavaScript. A tsconfig `paths` mapping works around it for users, but it
also makes the request match
the import map instead, so the two resolvers overlap; that deserves its
own decision and is tracked
separately rather than papered over here.

<!-- NEXT_JS_LLM -->

<!-- fleet 3acc7e63-52a8-4472-89cd-b1bbb8ad59fa -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-08-25 21:46:47 +02:00
Tobias Koppers d79b0d09cb fix(wasm): don't enable SWC's plugin host for wasm targets (#97859)
> Replaces #97788, which was **not merged**. Reordering this stack
briefly left that PR
> pointing at a base branch that had come to contain its own head
commit, so GitHub closed it
> as merged and deleted its branch. Nothing from it reached `canary`. It
had been approved;
> this PR is the same commit (`142f8882a2`), restored, and needs review
again. Sorry for the churn.

### What?

Stop enabling SWC's plugin-host Cargo features when building for wasm.

### Why?

`turbopack-ecmascript-plugins` and `next-napi-bindings` both requested
`swc_core`'s
`__plugin_transform_host` unconditionally, and that feature includes
**`swc/plugin`** directly. So
every wasm build pulled in SWC's plugin runner and compiled SWC's
`target_arch = "wasm32"` plugin
path — code that cannot run there.

SWC does not support executing wasm plugins from inside wasm (see
swc-project/swc#3934; its own wasm binding stubs the equivalent feature
out to `[]`), and hosting them
needs the wasmtime backend, which was already gated to non-wasm. The
plugin rule on the Next.js side
is native-only too. Only the Cargo features were never gated to match
the code boundary that already
existed.

This is what lets the rest of the stack build for wasm **without
patching SWC**: the wasm32 plugin arm
that does not compile is simply never built.

It also removes a portability hazard beyond that compile error —
`swc/plugin` brings in tokio's
multi-thread runtime, which wasi accepts only on the threaded target;
plain `wasm32-wasip1` rejects
those tokio features outright.

`plugin_transform_host_native_filesystem_cache` moves with them: it is a
*native* filesystem cache and
cannot apply on wasm.

### How?

Both crates keep their shared AST/transform features and move the
plugin-host features into their
existing non-wasm dependency blocks.

Verified on the resolved feature graph rather than by reading manifests,
because feature unification
can make a manifest look right while the graph is wrong:

- `cargo tree -p next-napi-bindings --target wasm32-wasip1-threads -e
features` — no
`swc feature "plugin"`, no `swc_core feature "__plugin_transform_host"`;
- the same query for the host target lists all six plugin-related
features, byte-identical to before.


<!-- fleet b6d0486f-97c7-42a7-bdaf-3490774cdec3 -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-25 16:23:04 +02:00
Tobias Koppers 319cdf2bc1 fix(turbopack-node): make process_pool inert on wasm (#97858)
> Replaces #97584, which was **not merged**. Reordering this stack
briefly left that PR
> pointing at a base branch that had come to contain its own head
commit, so GitHub closed it
> as merged and deleted its branch. Nothing from it reached `canary`. It
had been approved;
> this PR is the same commit (`fb0b97fd84`), restored, and needs review
again. Sorry for the churn.

### What?

`turbopack-node`'s `process_pool` feature is inert on wasm, leaving
`worker_pool` as the only Node
backend there.

### Why?

The child-process pool needs `tokio::process` and a TCP listener,
neither of which exists on wasi:

```
error[E0432]: unresolved import `tokio::process`   # gated #[cfg(not(target_os = "wasi"))] in tokio
error[E0599]: no `TcpListener::bind` on wasi
  --> turbopack/crates/turbopack-node/src/process_pool/mod.rs:315
```

Turning the feature off from the outside is not possible: `process_pool`
is a **default** feature of
both `turbopack-node` *and* `next-core`, so `--no-default-features` at
the top level does not
suppress it.

`worker_pool` — Node worker threads over napi — is already a first-class
alternative selected by
`TurbopackPluginRuntimeStrategy`, so no new mechanism is needed.

### How?

Gate the seven `process_pool` sites on `not(target_family = "wasm")`:
the module, the sealed backend
impl, the constructor, the config enum variant, the default-strategy
selection, and the `next-api`
import and match arm. Host feature semantics are unchanged.

<!-- NEXT_JS_LLM -->



<!-- fleet b6d0486f-97c7-42a7-bdaf-3490774cdec3 -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-25 16:23:03 +02:00
Tobias Koppers a3183c018b fix(turbopack): make the SWC wasm-plugin backend native-only (#97857)
> Replaces #97583, which was **not merged**. Reordering this stack
briefly left that PR
> pointing at a base branch that had come to contain its own head
commit, so GitHub closed it
> as merged and deleted its branch. Nothing from it reached `canary`. It
had been approved;
> this PR is the same commit (`7c99dff628`), restored, and needs review
again. Sorry for the churn.

### What?

Makes the SWC wasm-plugin backend native-only, and reports an **error**
against `next.config` when
`experimental.swcPlugins` is configured on a platform that cannot
execute them.

### Why?

`swc_plugin_backend_wasmtime` cannot be hosted inside wasm: there is no
JIT, and `wasi-common` needs
native filesystem APIs. SWC's own wasm32 path skips plugin transforms
for the same reason, see
swc-project/swc#3934.

Silently dropping the transform is the worst outcome — the build
succeeds and quietly produces
different output than the config asked for. A warning is not much
better: it still emits a bundle whose
code was not transformed as configured, which is subtly wrong rather
than obviously broken. So this
reports an error instead.

### How?

- `swc_plugin_backend_wasmtime` moves to a non-wasm dependency, and the
plugin rule's implementation is
  split into native and wasm arms.
- The wasm arm emits `UnsupportedSwcPluginsIssue` —
`IssueStage::Unsupported`, `IssueSeverity::Error`
— naming the configured plugins and pointing at the resolved
`next.config` path (via
`NextConfig::config_file_path`, as the babel and sass issues do), so the
message points at the file
  the user has to edit.

It fires once at config time and only when plugins are actually
configured, so projects that don't use
them are unaffected. Native behaviour is unchanged.


<!-- fleet b6d0486f-97c7-42a7-bdaf-3490774cdec3 -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-25 16:23:03 +02:00
Tobias Koppers 79b1300b80 fix(next-napi-bindings): detect the target, not the host, in build.rs (#97576)
### What?

`crates/next-napi-bindings/build.rs` now decides whether to emit
`-Wl,--warn-unresolved-symbols` from `CARGO_CFG_TARGET_OS` /
`CARGO_CFG_TARGET_FAMILY` instead of
from a `#[cfg(...)]` attribute.

### Why?

`#[cfg(...)]` inside a build script is evaluated for the machine that
**runs** the script — the host
— not for the target being compiled. The existing `not(target_arch =
"wasm32")` guard was therefore
always true on a Linux host, so cross-compiling to wasm still passed the
flag and the link died with:

```
rust-lld: error: unknown argument: -Wl,--warn-unresolved-symbols
```

This is a latent bug for any cross-compilation target, not only wasm.

### How?

Cargo sets `CARGO_CFG_*` for the *target*, so the condition is read from
the environment instead of
from a compile-time attribute. Behaviour on native Linux is unchanged.

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-25 14:13:55 +02:00
Benjamin Woodruff ‮ ef85120648 Turbopack: Improve file existence error handling in realpath_with_links and in module resolution (#97717)
Fixes some regressions from
https://github.com/vercel/next.js/pull/97395.

Human summary of changes:

- Plainly return `NotFound` for `realpath_with_link` whenever any step
in the resolution algorithm fails, matching the behavior the libc uses.
- When probing in the module resolution algorithm, explicitly check for
`NotFound`.
- Fully remove the extremely non-standard and confusing behavior of
trying to return the last processed path from `realpath_with_links` if a
failure occurs.
- Modify `realpath` to return a `RealPathError` instead of just an
`anyhow::Error`, so that callers that care about the actual error kind
but not all of the intermediate links can still use it instead of
`realpath_with_links`.
- Rename `RealPathResultError` to `RealPathError`, and make it actually
implement `Display` so that it's easier to coerce to an `anyhow::Error`.
This requires including a tiny bit more data on this object, but in the
hot path it's just one extra clone of a `FileSystemPath`.

Human discussion:
https://vercel.slack.com/archives/C09R44U5HQW/p1787322213695299

AI slop summary of why #97395 caused a regression:

> The regression came from newly added missing-target detection in
b2e835ded6.
>
> Before that commit, realpath_with_links:
>
> 1. Resolved directory symlinks.
> 2. Continued resolving the remaining path beneath the resolved
directory.
> 3. If that remaining path did not exist, returned the resolved path
successfully.
> 4. Let the caller’s subsequent existence check report that the
candidate was absent.
>
> The commit changed read_link so it no longer checked the symlink
target’s type. To detect dangling symlinks, realpath_with_links added
this condition:
>
> ```
> if (followed_link || !symlinks.is_empty())
>     && entry_type == FileSystemEntryType::NotFound
> {
>     // report a missing symlink target
> }
> ```
>
> The problem is that !symlinks.is_empty() does not mean the missing
path is a symlink target. It only means that some earlier path component
was a symlink.
>
> For the reported path:
>
> `node_modules/@workflow/core/dist/serialization/package.json`
>
> resolution worked like this:
>
> 1. Resolve node_modules/@workflow/core, a valid directory symlink.
> 2. Continue into dist/serialization, which exists.
> 3. Probe the optional dist/serialization/package.json, which does not
exist.
> 4. Because the earlier @workflow/core component was a symlink,
symlinks was nonempty.
> 5. The new condition incorrectly classified the missing package.json
as a dangling symlink target.
> 6. Turbopack aborted instead of treating package.json as an absent
resolution candidate and continuing to serialization.js.
>
> So the regression was not caused by resolving the directory symlink
incorrectly. It was caused by using “we traversed any symlink” as
evidence that a later missing descendant was the target of a dangling
symlink.
2026-08-24 21:12:28 +00:00