Commit Graph

1409 Commits

Author SHA1 Message Date
Tobias Koppers 143d9ca1ab feat(turbopack): change DiskFileSystem root to Vc<RcStr> for portable cache (#92603)
### What?

Changes the `root` parameter of `DiskFileSystem::new`,
`DiskFileSystem::new_with_denied_paths`, and the internal
`DiskFileSystem::new_internal` from `RcStr` to `Vc<RcStr>`.

### Why?

Part of the effort to make the Turbopack persistent cache portable when
the absolute path of the project directory changes. By accepting the
root as a `Vc<RcStr>` instead of a plain `RcStr`, the path is tracked as
a turbo-tasks value. This allows the cache to remain valid even when the
project root moves (e.g. on a different machine, a different CI working
directory, or a relocated checkout), since the value can be substituted
independently without invalidating unrelated cached computations.

### How?

- `new_internal` is now an `async fn` that awaits the `Vc<RcStr>` to
extract the underlying `RcStr` for constructing `DiskFileSystemInner`.
It returns `Result<Vc<Self>>` accordingly.
- `new` and `new_with_denied_paths` (the public constructors, which are
not `#[turbo_tasks::function]` themselves) are updated to accept
`Vc<RcStr>` and forward it to `new_internal`.
- All 23 call sites across the codebase are updated to wrap their root
string with `Vc::cell(...)` before passing it.

Verified with `cargo clippy --all-targets` (clean), `cargo test -p
turbo-tasks-backend` (all pass), and `cargo test -p turbopack-tests`
(all 89 pass).

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 07:55:58 +02:00
Tobias Koppers 74cdcde87e feat(turbopack): add LocalPathOrProjectPath PostCSS config resolution (#91338)
### What?

Adds a new `experimental.turbopackLocalPostcssConfig` option and
corresponding `PostCssConfigLocation::LocalPathOrProjectPath` variant so
Turbopack can resolve `postcss.config.js` starting from the CSS file's
directory first, falling back to the project root.

Also fixes an issue that surfaces when multiple per-directory
`postcss.config.js` files are used:

**Evaluate pool asset name collisions** — output file names for the
Node.js evaluate pool are now derived from an xxh3 hash of the full
module ident rather than the bare file name, so multiple
`postcss.config.js` files in different directories no longer collide.

### Why?

Currently Turbopack uses `ProjectPathOrLocalPath` which checks the
project root first, then falls back to the local directory. This means
per-directory `postcss.config.js` files cannot override the root config
— the root config always wins. For projects that need different PostCSS
transforms in different directories (e.g. a monorepo with multiple apps
or style directories), the more specific config should take precedence.

### How?

**Turbopack PostCSS resolution**
(`turbopack/crates/turbopack-node/src/transforms/postcss.rs`):
- Added `PostCssConfigLocation::LocalPathOrProjectPath` enum variant
with doc comments on all variants
- Refactored `find_config_in_location()` to express the search order as
a `Vec<FileSystemPath>` built from a single `match`, making each
variant's strategy immediately clear

**Evaluate pool asset file naming**
(`turbopack/crates/turbopack-node/src/evaluate.rs`):
- Replaced file-name-based output path derivation with a deterministic
xxh3 hash of the full module ident
- Adds `turbo-tasks-hash` dependency to `turbopack-node`
- Eliminates name collisions when multiple entries share the same file
name (e.g. per-directory `postcss.config.js` files)

**Next.js config flag** (`packages/next/src/server/config-shared.ts`,
`config-schema.ts`):
- Added `experimental.turbopackLocalPostcssConfig: boolean` option
(opt-in, default `false`)
- Wired through `crates/next-core/src/next_config.rs` → both client and
server contexts (`next_client/context.rs`, `next_server/context.rs`)
- When `true`, uses `LocalPathOrProjectPath`; when `false`/unset, uses
the existing `ProjectPathOrLocalPath` (no behavior change by default)

**E2E test** (`test/e2e/app-dir/turbopack-postcss-multiple-configs/`):
- Runs in both **Turbopack dev and production modes** — skips webpack
only (webpack does not support function-valued PostCSS plugins and the
`turbopackLocalPostcssConfig` flag is Turbopack-specific)
- 5 style directories, each with its own `postcss.config.js` passing a
**unique color option** (`blue`, `purple`, `orange`, `cyan`, `magenta`)
to a shared PostCSS plugin, proving that per-directory config resolution
produces distinct results rather than all sharing the same transform
- Root `postcss.config.js` is a no-op — if only the root config were
used, CSS would remain unmodified
- Test validates all 15 elements (5 dirs × 3 files) render with CSS
module classes
- Test collects all CSS (inline `<style>` tags for dev, linked `.css`
files for production) and asserts each directory's expected color is
present and `color: red` is absent

**Documentation**
(`docs/01-app/03-api-reference/05-config/01-next-config-js/turbopackLocalPostcssConfig.mdx`,
`docs/01-app/03-api-reference/08-turbopack.mdx`):
- New dedicated reference page for
`experimental.turbopackLocalPostcssConfig` covering usage, behavior
table, and a directory-tree example
- Added to the experimental options table in the Turbopack API reference

## Test plan

- [x] `cargo check -p turbo-tasks-backend` passes
- [x] `cargo check -p turbopack-node` and `cargo check -p next-core`
pass
- [x] E2E test `turbopack-postcss-multiple-configs` validates
per-directory PostCSS config resolution across 5 directories with
distinct per-directory colors (Turbopack dev and production modes)
- [x] Test correctly skips in webpack mode and deployment
- [x] Documentation page and experimental options table updated

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
2026-04-11 07:55:10 +02:00
Luke Sandberg 35b5582647 Remove a ton of turbo task functions from Issue trait items (#92623)
## What?

Remove `#[turbo_tasks::function]` from all methods on the `Issue` value_trait, following the same pattern as #92593 which did the same for `ValueDebug`.

The `Issue` trait previously had 8 methods decorated with `#[turbo_tasks::function]`, which caused the turbo-tasks machinery to generate `NativeFunction` statics, task registration, vtable entries, and scheduling/caching infrastructure for each method on each of the ~48 `impl Issue for` types in the codebase — most of which are trivially synchronous (returning a constant string, enum variant, or stored struct field).

## How?

- Change all 8 `Issue` trait methods to plain methods. Methods that are always synchronous become direct return-type methods (`severity`, `stage`, `source`, `documentation_link`). Methods that are genuinely async on at least one impl use `Pin<Box<dyn Future<Output = Result<T>> + Send + '_>>` for object safety.
- Callers that need to invoke trait methods on a `Vc<Box<dyn Issue>>` use `issue.into_trait_ref().await?` to get a `TraitRef<Box<dyn Issue>>` and call methods directly on that.
- `PlainIssue::from_issue` (the sole hot caller that materializes all fields) resolves one `trait_ref` and calls all methods on it, similar to the `ValueDebug` pattern.
- `ResolvingIssueWithLocation` (which wraps an inner `ResolvedVc<Box<dyn Issue>>`) eagerly copies `severity`, `stage`, `documentation_link`, and `source` at construction time rather than delegating async calls to the inner issue.
- Delete `OptionIssueSource` and `OptionStyledString` wrapper types, which existed solely as `Vc<T>` return type wrappers for the old turbo-tasks methods.

## Tradeoffs

**Lost per-call caching:** Previously each `Issue` method was a turbo-tasks task, so results were cached per `(Vc<Issue>, method)`. In practice this caching was useless — issues are emitted once and their fields read once by `PlainIssue::from_issue`, so there was never a second call to hit the cache. The only real cost is that `IssueFilter::matches` previously cached per `(filter, issue)` pair; that caching is preserved because `matches` itself remains a `#[turbo_tasks::function]` and now calls `into_trait_ref().await?` once at the top.

**`ResolvingIssueWithLocation` eagerly reads inner fields:** Instead of lazily delegating to the inner issue, `stage` and `documentation_link` are copied at construction. This is a minor semantic change (the values are snapshotted rather than live), but in practice these fields are always constant values so it makes no difference.

## Binary size impact (darwin-arm64, compared against #92593)

| Metric | #92593 baseline | This PR | Delta |
|---|---|---|---|
| Unstripped dylib | 124.6 MB | 124.0 MB | **-0.6 MB (-0.5%)** |
| Stripped | 84.7 MB | 84.4 MB | **-0.3 MB (-0.4%)** |
| Stripped + gzip (npm) | 29.2 MB | 29.1 MB | **-0.1 MB (-0.3%)** |

Smaller savings than `ValueDebug` (~2.5 MB stripped) because there are ~48 `Issue` impls vs. one `ValueDebug` impl per every `#[turbo_tasks::value]` type. But real and cumulative.

<!-- NEXT_JS_LLM_PR -->
2026-04-10 16:56:47 -07:00
Niklas Mischkulnig e934009e0e Turbopack: cut number of client references in half (#92607)
Currently, every client reference exists twice, but one of them is always dead code.

This shouldn't have an impact on the responses sent to the browser, but the server code is smaller. Doesn't have a measureable impact on cold build time.

For one page, this shrank page_client-reference-manifest.js from 979k to 742k

```js
"[project]/app/[slug]/counter-2.js [app-rsc] (client reference proxy) <module evaluation>", ((__turbopack_context__) => {
"use strict";

// UNUSED
__turbopack_context__.s(["default",()=>__TURBOPACK__default__export__]);
var reactDom = __turbopack_context__.i("react-server-dom-turbopack-server.js [app-rsc] (ecmascript)");
const __TURBOPACK__default__export__ = (0, reactDom["registerClientReference"])(function() {
    throw new Error("Attempted to call the default export of [project]/app/[slug]/counter-2.js <module evaluation> from the server, but it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.");
}, "[project]/app/[slug]/counter-2.js <module evaluation>", "default");

}),
"[project]/app/[slug]/counter-2.js [app-rsc] (client reference proxy)", ((__turbopack_context__) => {
"use strict";

__turbopack_context__.s(["default",()=>__TURBOPACK__default__export__]);
var reactDom = __turbopack_context__.i("react-server-dom-turbopack-server.js [app-rsc] (ecmascript)");
const __TURBOPACK__default__export__ = (0, reactDom["registerClientReference"])(function() {
    throw new Error("Attempted to call the default export of [project]/app/[slug]/counter-2.js from the server, but it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.");
}, "[project]/app/[slug]/counter-2.js", "default");

}),
"[project]/app/[slug]/counter-2.js [app-rsc] (ecmascript)", ((__turbopack_context__) => {
"use strict";


// UNUSED:
var client__reference__proxy$29$__$3c$module__evaluation$3e$__ = __turbopack_context__.i("[project]/app/[slug]/counter-2.js [app-rsc] (client reference proxy) <module evaluation>");
// Only this is exported
var client__reference__proxy$29$__ = __turbopack_context__.i("[project]/app/[slug]/counter-2.js [app-rsc] (client reference proxy)");
;
__turbopack_context__.n(client__reference__proxy$29$__);
}),

"[project]/app/[slug]/page.js [app-rsc] (ecmascript)", ((__turbopack_context__) => {

var counter = __turbopack_context__.i("[project]/app/[slug]/counter-1.js [app-rsc] (ecmascript)");

```
and also twice in the manifest
```js
    '[project]/bench/client-refs/app/[slug]/counter-1.js <module evaluation>': {
      id: '[project]/bench/client-refs/app/[slug]/counter-1.js [app-client] (ecmascript)',
      name: '*',
      chunks: [
        '/_next/static/chunks/2li6a5uw43ak2.js',
        '/_next/static/chunks/08svqy8591o8a.js',
      ],
      async: false,
    },
    '[project]/bench/client-refs/app/[slug]/counter-1.js': {
      id: '[project]/bench/client-refs/app/[slug]/counter-1.js [app-client] (ecmascript)',
      name: '*',
      chunks: [
        '/_next/static/chunks/2li6a5uw43ak2.js',
        '/_next/static/chunks/08svqy8591o8a.js',
      ],
      async: false,
    },
```
2026-04-10 16:00:01 -07:00
Luke Sandberg 6344e18ac1 Turbopack: extract inline base64 decode helper to shared @turbopack/base64 module (#92549)
### What?

When Turbopack generates a module using `import foo from './blah' with {type:'bytes'}`, it needs a base64 decode helper. Previously this was inlined into every generated module. This PR extracts it to a shared `@turbopack/base64` module imported via a special import map, and encapsulates the import map registration inside the turbopack crate itself.

### Why?

1. **Deduplication** — The base64 decode helper is shared across all `{type:'bytes'}` imports instead of being duplicated inline in every generated module.
2. **Encapsulation** — The `@turbopack/*` and `@vercel/turbopack-ecmascript-runtime/*` import map aliases are now injected automatically by `ModuleAssetContext::resolve_options` in the turbopack crate. Consumers (next-core, turbopack-cli) no longer need to manually register these aliases in their import maps.
3. **Extensibility** — Future turbopack built-in modules can be added to a single `turbopack_runtime_import_map()` function, with no changes needed in any consumer.

### How?

**Base64 module (`turbopack-ecmascript-runtime`):**
- Added `turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/base64.ts` with `base64Decode()` using ES2024 `Uint8Array.fromBase64` with an `atob()`-based fallback.
- The ES2024 feature check is evaluated once at module load time (top-level const), not per-call.
- The module is marked `"use turbopack no side effects"` for tree-shaking.

**BytesSourceTransform (`turbopack-ecmascript`):**
- Changed `BytesSourceTransform` to emit `import { base64Decode } from '@turbopack/base64'` instead of inlining the helper.

**Import map encapsulation (cycle-breaking refactor):**
- Moved `get_runtime_asset_context` from `turbopack-ecmascript-runtime` → `turbopack` crate, breaking the cyclic dependency so turbopack can now depend on `turbopack-ecmascript-runtime`.
- Changed `get_nodejs_runtime_code` and `get_browser_runtime_code` to accept an `AssetContext` parameter instead of constructing one internally.
- Added `turbopack_runtime_import_map()` to `turbopack-ecmascript-runtime` containing both the `@vercel/turbopack-ecmascript-runtime/*` wildcard and `@turbopack/base64` exact aliases.
- `ModuleAssetContext::resolve_options` now injects this import map automatically.
- Removed `insert_turbopack_dev_alias` from `next-core` and the equivalent block from `turbopack-cli`.

<!-- NEXT_JS_LLM_PR -->
2026-04-10 08:45:37 -07:00
Tobias Koppers 7d09d1e605 Fix NextFontIssue IssueStage: CodeGen → Resolve (#92588)
### What?

Changes `NextFontIssue`'s `IssueStage` from `CodeGen` to `Resolve`.

### Why?

`NextFontIssue` is emitted during font resolution — when validating font
configuration options (`util.rs`) and when fetching/resolving Google
Font metadata (`google/mod.rs`, `google/font_fallback.rs`). None of
these occur during code generation; they all happen at the resolve stage
of the build pipeline.

Using `IssueStage::CodeGen` was incorrect and caused these font issues
to be grouped/sorted with code generation errors rather than resolution
errors. The local-font counterpart (`FontResolvingIssue` in
`next_font/local/mod.rs`) already correctly uses `IssueStage::Resolve`.

### How?

Single-line fix: change `IssueStage::CodeGen.cell()` to
`IssueStage::Resolve.cell()` in the `stage()` method of `impl Issue for
NextFontIssue`.

<!-- NEXT_JS_LLM_PR -->

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-10 10:47:04 +02:00
Luke Sandberg a24462d9e5 Replace wasmer with wasmtime for SWC plugin WASM engine (#91533)
## What?

Replace wasmer with wasmtime as the WASM runtime for SWC plugins.

## Why?

- Wasmer has been a source of build complexity due to conflicting feature flags (`wasmer/default` vs `wasmer/js-default`) and large binary size
- Wasmtime is actively maintained by the Bytecode Alliance with better performance and smaller footprint
- The upstream SWC project already provides `swc_plugin_backend_wasmtime` on crates.io, so we can use it directly instead of maintaining our own implementation

## How?

- Use the upstream `swc_plugin_backend_wasmtime` v9.0.0 crate from crates.io instead of a custom local implementation
- Replace `swc_core/plugin_transform_host_native` (which pulls in wasmer via `__plugin_transform_env_native`) with granular features: `__plugin_transform_host` + `__plugin_transform_host_schema_v1`
- Provide `WasmtimeRuntime` explicitly in both the NAPI bindings and turbopack plugin paths
- Remove all direct `wasmtime`/`wasmtime-wasi` workspace dependencies — the upstream crate manages its own wasmtime version
- Replace wasmer-related release profile size optimizations with equivalent wasmtime ones (`swc_plugin_backend_wasmtime`, `swc_plugin_runner`, `wasmtime`, `wasmtime-internal-cranelift`)
- Pin `vergen = "=9.0.6"` to avoid a `vergen-lib` version conflict with `vergen-gitcl` 1.0.8

## Binary size impact (darwin-arm64, compared against current canary)

| Metric | Canary | Wasmtime (this PR) | Delta |
|---|---|---|---|
| Unstripped dylib | 133.1 MB | 128.2 MB | **-5.0 MB (-3.7%)** |
| Stripped | 91.2 MB | 86.9 MB | **-4.3 MB (-4.8%)** |
| Stripped + gzip (npm) | 30.9 MB | 29.4 MB | **-1.5 MB (-5.0%)** |

<!-- NEXT_JS_LLM_PR -->
2026-04-09 09:34:00 -07:00
Jiwon Choi 7e741b4207 Handle target option in React Compiler for Pages + React 18 (#91500)
### Why?

`test/e2e/app-dir/catch-error/catch-error-react-compiler.test.ts` is
skipped in CI due to incompatibility with React 18 tests.

```
Module not found: Can't resolve 'react/compiler-runtime'
> 1 | import { c as _c } from "react/compiler-runtime";
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

Failed CI:
https://github.com/vercel/next.js/actions/runs/23170738194/job/67327462789#step:36:317
x-ref:
https://react.dev/reference/react-compiler/target#targeting-react-17-or-18

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <12292047+eps1lon@users.noreply.github.com>
Co-authored-by: Benjamin Woodruff <180404+bgw@users.noreply.github.com>
Co-authored-by: Sebastian Silbermann <eps1lon@users.noreply.github.com>
2026-04-09 15:21:29 +00:00
Tim Neutkens 87e8b29700 Turbopack: Improve unable to resolve next error (#91394)
## What

Improves the error message shown when Turbopack cannot resolve the
`next` package. The issue now includes the filesystem root used for
resolution, making it easier to diagnose misconfigured `turbopack.root`
settings.

## Why

When `next` can't be resolved, the previous error message showed the
context directory but not the root used for resolution. This made it
difficult to determine whether the resolution root was incorrect,
especially in monorepo setups.

## Changes

- Added `root` field to `MissingNextFolderIssue` to track the filesystem
root used for resolution
- Display the filesystem root path in the error message
- Updated the fix suggestion to reference "project directory" instead of
"this directory" for clarity
2026-04-09 15:48:49 +02:00
Niklas Mischkulnig 7ac71949ab Turbopack: switch from immutable token to path prefix (#92164) 2026-04-08 22:54:08 +02:00
Aurora Scharff 108fe00fbd fix: replace docs/canary links with stable docs paths in error messages (#92528)
### What?

Error messages for the `"use cache"` directive and `after()` API (in
`cookies()`, `headers()`, `connection()`) pointed to `/docs/canary/...`
instead of `/docs/...`.

### Why?

On canary builds, `docs/canary` links can create circular or confusing
references. These APIs are stable and their docs live at the standard
`/docs/app/...` path.

### How?

Replaced all `nextjs.org/docs/canary/` URLs with `nextjs.org/docs/` in:

- `crates/next-custom-transforms/src/transforms/server_actions.rs` — the
`"use cache"` feature flag error
-
`crates/next-custom-transforms/tests/errors/use-cache-not-allowed/1/output.stderr`
and `2/output.stderr` — matching test fixtures
- `packages/next/src/server/request/cookies.ts`, `headers.ts`,
`connection.ts` — `after()` errors
- `packages/next/errors.json` — compiled error message index (6
occurrences)

<!-- NEXT_JS_LLM_PR -->
2026-04-08 20:28:46 +02:00
Tim Neutkens c5596dd4e0 Turbopack: Write trace file to .next-profiles (#92189)
## What?

Builds on #92078. Also writes the Turbopack trace to the same
.next-profiles directory.
2026-04-08 16:50:17 +02:00
Tobias Koppers 1297f606be perf: use whole_app_module_graph for get_compilation_issues MCP tool (#92473)
### What?

Optimizes the `get_compilation_issues` MCP tool to use a single
whole-app module graph computation instead of iterating all endpoint
groups and computing per-route module graphs individually.

### Why?

The `get_compilation_issues` MCP tool was too slow for large apps
because `get_all_compilation_issues_inner_operation` called
`endpoint_group.module_graphs()` for every endpoint group, computing
separate per-route module graphs. This is O(routes) graph computations
when a single whole-app graph would suffice.

The reason the original code didn't use `whole_app_module_graphs()` was
that it calls `drop_issues()` in development mode (to prevent every
route from re-reporting all issues during HMR). This would suppress the
issues the MCP tool needs to collect.

### How?

- **New method
`Project::whole_app_module_graphs_without_dropping_issues()`** — calls
the same underlying `whole_app_module_graph_operation` (the
`#[turbo_tasks::function(operation, root)]` computation) but connects
the result without dropping issues. Issues propagate normally so callers
can collect them.
- **`get_all_compilation_issues_inner_operation`** now calls this new
method instead of iterating endpoint groups, reducing the work to a
single graph computation.
- **Extracted `scale_down_node_pool` helper** — the Node.js process pool
scale-down logic (shared between both `whole_app_module_graphs`
variants) is extracted into a free function to avoid duplication.
- **Existing `whole_app_module_graphs()`** is unchanged in behavior —
still drops issues in dev mode for HMR per-route subscriptions.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 16:32:56 +02:00
Andrew Imm 5636400139 Adding more system info to the 'initialize project' trace (#92427) 2026-04-07 20:56:46 +02:00
Benjamin Woodruff df6cd599b3 Turbopack: Remove turbo_tasks::apply_effects, use Effects::apply instead (#91858)
Having both `apply_effects` and `Effects::apply` is confusing.

`apply_effect`'s API seems a bit bad/dangerous because it combines work that should be done inside of a strongly-consistent operation (collecting the effects and reading their `Vc`s) with work that should be done at the top-level outside of any operation or turbo-task function (applying the effects).

The first commit in this PR removes the `apply_effect` API and tries to improve documentation, including adding a doctest that compiles.

The second commit in this PR is an LLM-driven refactor to remove the `apply_effect` callsites.
2026-04-06 20:53:39 -07:00
Tobias Koppers 81d5e070f4 next-core: deduplicate output assets and detect content conflicts on emit (#92292)
### What?

Adds deduplication and conflict detection to the asset emission stage in
`crates/next-core/src/emit.rs`, and a new `IssueStage::Emit` variant in
`turbopack-core`.

Before emitting, assets are grouped by their output path. If multiple
assets map to the same path:

- If their content is identical, one is silently chosen (deduplication).
- If their content differs, both versions are written to
`<node_root>/<content_hash>.<ext>` and an `EmitConflictIssue` is raised
for each conflict. All assets are still emitted — conflicts do not abort
the build.

### Why?

Previously, duplicate output assets for the same path were emitted
unconditionally — whichever write happened last silently won. This
masked build graph bugs where two different modules produced conflicting
output files. Reporting conflicts as issues (rather than silently
overwriting) makes them visible and easy to diagnose without breaking
the build.

### How?

- Collect all assets with their resolved paths via `try_flat_join`.
- Bucket them into two `FxIndexMap<FileSystemPath,
Vec<ResolvedVc<Box<dyn OutputAsset>>>>` — one for node-root assets and
one for client assets.
- For each bucket entry, call `check_duplicates`: compare every asset
against the first using `assets_diff`. If content differs, emit an
`EmitConflictIssue` as a turbo-tasks collectible — but still return the
first asset so emission continues.
- `assets_diff` is a `#[turbo_tasks::function]` that takes only
`(asset1, asset2, extension, node_root)` — the `asset_path` stays out of
the task key to avoid unnecessary task cardinality. When file content
differs, it hashes each version with xxh3, writes them to
`<node_root>/<hash>.<ext>`, and returns the paths in the detail message
so the user can diff them.
- `EmitConflictIssue` implements the `Issue` trait with
`IssueStage::Emit` (new variant added to `turbopack-core`),
`IssueSeverity::Error`, a descriptive title, and a detail message
explaining the type of conflict.
- Node-root and client assets are emitted in parallel via
`futures::join!` (not `try_join!`) to ensure deterministic error
reporting — both branches always run to completion so errors are
reported in a consistent order.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 20:37:58 +02:00
Tobias Koppers f65b10a54d turbo-tasks: replace async resolve fns with custom Future types (ResolveRawVcFuture, ResolveVcFuture, ToResolvedVcFuture) (#91554)
### What?

Replace the `async fn resolve()`, `async fn
resolve_strongly_consistent()`, and `async fn to_resolved()` methods on
`RawVc`, `Vc<T>`, and `OperationVc<T>` with hand-written custom `Future`
implementations, following the existing `ReadRawVcFuture` pattern.

New types:
- **`ResolveRawVcFuture`** (`raw_vc.rs`) — core implementation, replaces
`async fn resolve_inner()`
- **`ResolveVcFuture<T>`** (`vc/mod.rs`) — typed wrapper over
`ResolveRawVcFuture`, returned by `Vc::resolve()`
- **`ResolveOperationVcFuture<T>`** (`vc/operation.rs`) — typed wrapper,
returned by `OperationVc::resolve()`
- **`ToResolvedVcFuture<T>`** (`vc/mod.rs`) — typed wrapper, returned by
`Vc::to_resolved()`

All new future types expose a `.strongly_consistent()` builder method,
enabling `resolve_strongly_consistent()` to be replaced by
`.resolve().strongly_consistent()` at call sites.

`ReadRawVcFuture` is also updated to delegate its phase-1 resolve loop
to `ResolveRawVcFuture` instead of duplicating the logic.
`std::task::ready!` is used throughout to simplify poll implementations.

Also adds `#[inline(never)]` to `ReadRawVcFuture::poll` and
`ResolveRawVcFuture::poll` to avoid inlining large poll implementations
into every await site.

### Why?

Performance, binary size, and improved API ergonomics:

- The hand-written `Future` pattern (already used by `ReadRawVcFuture`)
gives the compiler more predictable, smaller code than the state
machines generated for `async fn`. The `#[inline(never)]` attributes on
`poll` prevent large poll bodies from being duplicated at every await
site, which the async desugaring otherwise allows.
- The new builder API (`.resolve().strongly_consistent()`) is more
composable and removes the need for separate `_strongly_consistent`
method variants, reducing the number of methods on
`RawVc`/`Vc`/`OperationVc`.
- Having `ReadRawVcFuture` delegate to `ResolveRawVcFuture` removes the
duplicated resolve loop and ensures both paths stay in sync.

### How?

- `ResolveRawVcFuture` stores `current: RawVc`, `read_output_options:
ReadOutputOptions`, `strongly_consistent: bool`, and `listener:
Option<EventListener>`. Its `poll` replicates the loop from the old
`resolve_inner` using `try_read_task_output` / `try_read_local_output`.
- On `Err(listener)` from a `try_*` call, the listener is stored in
`self.listener` and `Poll::Pending` is returned. At the top of the loop,
`ready!(poll_listener(...))` re-polls it and short-circuits if still
pending.
- Consistency is downgraded to `Eventual` after the first `TaskOutput`
hop, matching the previous behavior.
- `strongly_consistent: true` keeps the
`SUPPRESS_EVENTUAL_CONSISTENCY_TOP_LEVEL_TASK_CHECK` suppression across
all polls (same logic as `ReadRawVcFuture`).
- `ReadRawVcFuture` now holds a `ResolveRawVcFuture` for phase 1 and
drives it via `Pin::new(&mut self.resolve).poll(cx)` before proceeding
to the cell read in phase 2. This eliminates the duplicated loop that
previously existed in both types.
- Typed wrappers (`ResolveVcFuture<T>`, `ResolveOperationVcFuture<T>`,
`ToResolvedVcFuture<T>`) delegate `poll` to the inner
`ResolveRawVcFuture` and map the output to the appropriate typed result.
- `OperationVc::resolve_strongly_consistent()` is removed; 16 call sites
updated to `.resolve().strongly_consistent()`.
- All new types implement `Unpin` and are exported from `lib.rs`.
- `std::task::ready!` is used in all `poll` implementations to reduce
boilerplate.

No behavioral changes — this is a pure implementation refactor.

### Binary size impact

A release build (`pnpm swc-build-native --release`) was measured before
and after the branch changes on the same merge-base commit (`a41bef94`):

| | Size |
|---|---|
| Base (`a41bef94`, before branch) | 199,690,656 bytes (~190.4 MB) |
| Branch (`6f7846f9`, after changes) | 199,252,384 bytes (~190.0 MB) |
| **Difference** | **−438,272 bytes (−428 KB, −0.22%)** |

The branch produces a slightly smaller binary. The reduction comes
primarily from the `#[inline(never)]` attributes preventing large `poll`
bodies from being duplicated at every await site.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 08:11:57 +02:00
Benjamin Woodruff f6663b75fd Turbopack: Dedupe our two filesystempath extension/extension_ref functions (#92048)
PR is AI-generated

- `extension` and `extension_ref` do the same thing.
- `extension_ref`'s API that returns an `Option` was better than
`extension`'s API that returns a potentially-empty string.
- In some places we were splitting into a stem and extension, and
joining them back to a filename, but those places could just get the
original path's filename.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Benjamin Woodruff <bgw@users.noreply.github.com>
2026-04-03 21:51:33 -07:00
Jason Tan 2bf38b0115 feat: add experimental.swcEnvOptions for SWC preset-env configuration (#92272)
### What?

Add `experimental.swcEnvOptions` to expose SWC's preset-env `env`
configuration options — including `mode`, `coreJs`, `include`,
`exclude`, `skip`, `shippedProposals`, `forceAllTransforms`, `debug`,
and `loose`.

### Why?

Currently Next.js only passes `env.targets` (derived from browserslist)
to SWC for **syntax downleveling**, but does not expose the polyfill
injection capabilities that SWC already supports. This means:

- Users who need automatic core-js polyfills (e.g.
`Array.prototype.at()`, `Promise.withResolvers()`, `Set` methods) have
no built-in way to get them.
- The only workarounds are importing `core-js` globally (which bloats
bundles significantly) or ejecting to Babel with `useBuiltIns: 'usage'`
(which sacrifices SWC's performance benefits).
- In the Babel era, Next.js supported this via `@babel/preset-env`'s
`useBuiltIns` (PR #10574). That capability was lost when Next.js
migrated to SWC.

### How?

A new `experimental.swcEnvOptions` config is added. Its properties are
spread into the `env` block that Next.js passes to SWC for client-side
compilation, alongside the existing browserslist-derived `targets`.
Server-side compilation is unaffected (always targets `node`).

The option surface mirrors [SWC's preset-env
docs](https://swc.rs/docs/configuration/supported-browsers) 1:1, keeping
it familiar and forward-compatible.

```js
// next.config.js
module.exports = {
  experimental: {
    swcEnvOptions: {
      mode: 'usage',
      coreJs: '3.38',
    },
  },
}
```
#### Changes:
config-shared.ts — type definition with JSDoc
config-schema.ts — zod validation
next-swc-loader.ts → swc/options.ts — plumb config into the SWC env
block
Unit tests (6 cases) + e2e test (dev & production)

Related issues #66562, #63104, #74978
Related discussion #46724

---------

Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
2026-04-03 20:27:04 -07:00
Benjamin Woodruff 3c301acc32 Turbopack: Remove FileSystemPath::value_to_string in favor of ValueToStringRef::to_string_ref (#92284)
`FileSystemPath::value_to_string` was just calling `ValueToString::to_string`.

But:
- It's better to call `ValueToStringRef::to_string_ref`
- We don't really get much value from this inherent method except for avoiding a few imports?

I suspect this is left over from when we used to always wrap `FileSystemPath` in `Vc`.
2026-04-03 19:08:01 -07:00
Will Binns-Smith d6906eabd6 Update Rust toolchain to nightly-2026-04-02 (#92288) 2026-04-03 17:27:59 -07:00
Tobias Koppers 41839289de Add MCP get_compilation_issues tool and NAPI method (#92062)
### What?

Adds a new \`get_compilation_issues\` MCP tool (and the underlying
\`projectGetAllCompilationIssues\` NAPI method) that returns all
compilation issues from all routes in a single call.

**New files:**
- \`crates/next-napi-bindings/src/next_api/project.rs\` —
\`project_get_all_compilation_issues\` NAPI function +
\`get_all_compilation_issues_operation\` /
\`get_all_compilation_issues_inner_operation\` turbo-tasks operations
- \`packages/next/src/server/mcp/tools/get-compilation-issues.ts\` — MCP
tool implementation
-
\`packages/next/src/server/mcp/tools/utils/format-compilation-issues.ts\`
— output formatter
-
\`test/development/mcp-server/mcp-server-get-compilation-issues.test.ts\`
— e2e test
- \`test/development/mcp-server/fixtures/compilation-errors-app/\` —
fixture app with three routes (valid page, missing-module error, syntax
error)

**Modified files:**
- \`packages/next/src/build/swc/generated-native.d.ts\` + \`types.ts\` +
\`index.ts\` — expose the new NAPI method
- \`packages/next/src/server/mcp/get-or-create-mcp-server.ts\` —
register the new tool, accept \`getTurbopackProject\` option
- \`packages/next/src/server/dev/hot-reloader-turbopack.ts\` — pass
\`project\` to MCP middleware
- \`packages/next/src/telemetry/events/build.ts\` — add
\`'mcp/get_compilation_issues'\` to the \`McpToolName\` union

### Why?

The existing MCP tools require a browser session (and thus a specific
route to be rendered) to surface compilation errors.
\`get_compilation_issues\` works without a browser session and covers
all routes proactively — useful for AI coding agents that want to check
for errors across the whole app before trying to render a page.

### How?

**NAPI layer:**  
\`project_get_all_compilation_issues\` calls a two-level turbo-tasks
operation pair:

1. \`get_all_compilation_issues_inner_operation\` — iterates all
endpoint groups via \`project.get_all_endpoint_groups(false)\` and calls
\`endpoint_group.module_graphs().as_side_effect()\` on each. This builds
the module graph (resolution + transformation) for every entrypoint
without chunking, emitting, or code generation. Issues are emitted as
turbo-tasks collectables.

2. \`get_all_compilation_issues_operation\` — wraps the inner op in
\`strongly_consistent_catch_collectables\` to harvest
issues/diagnostics/effects, then returns an \`OperationResult\`.

> **Why not \`project.whole_app_module_graphs()\`?**  
> \`whole_app_module_graphs()\` calls \`drop_issues()\` in development
mode to prevent every per-route HMR subscription from seeing all global
issues. Calling it here would return zero issues in dev. Per-endpoint
\`module_graphs()\` calls don't have this suppression.

**Output formatting (\`format-compilation-issues.ts\`):**  
The raw Turbopack wire types are transformed before being returned to
MCP consumers:

- **StyledString → plain string**: \`title\`, \`description\`, and
\`detail\` are \`StyledString\` union trees (recursive \`{type, value}\`
objects). These are flattened to plain strings —
\`text\`/\`code\`/\`strong\` variants return their \`.value\` directly,
\`line\` joins with \`""\`, \`stack\` joins with \`"\n"\`.
- **ANSI codes stripped**: \`codeFrame\` is a pre-rendered string from
the Rust NAPI layer that contains ANSI terminal colour codes. These are
stripped via \`next/dist/compiled/strip-ansi\`.
- **1-indexed source positions**: Turbopack's \`SourcePos\` is 0-indexed
for both \`line\` and \`column\`. The formatter adds \`+1\` to each so
consumers get the conventional editor-style 1-indexed values.
- **Deduplication**: the same issue can surface from multiple endpoints
during the module graph traversal. Issues are deduplicated by a
\`severity|filePath|title|startLine:startCol\` key.
- **No diagnostics**: Turbopack diagnostics are internal telemetry
(\`EVENT_BUILD_FEATURE_USAGE\` feature-adoption counters). They are not
actionable for the user, so the \`diagnostics\` field is omitted
entirely from the response.

**MCP tool (\`get_compilation_issues\`):**  
Calls \`project.getAllCompilationIssues()\`, passes the issues through
\`formatCompilationIssues()\`, and returns \`{ issues: FormattedIssue[]
}\` as JSON. Works without a browser session.

**Test:**  
A fixture app with three routes (valid \`app/page.tsx\`,
\`app/missing-module/page.tsx\` importing a non-existent package,
\`app/syntax-error/page.tsx\` with unclosed JSX) is used to verify:
1. The tool returns a result without any browser session
2. Module-not-found errors are detected
3. Syntax errors are detected
4. Issues include \`severity\`, \`title\`, \`filePath\` metadata — all
as plain strings (not StyledString objects)

e2e tests: added —
\`test/development/mcp-server/mcp-server-get-compilation-issues.test.ts\`

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 14:38:44 +02:00
Tobias Koppers 3e0158846e feat: add NEXT_HASH_SALT env var for content-hash filename salting (#91871)
### What?

Adds a `NEXT_HASH_SALT` environment variable **and** a
`experimental.outputHashSalt` config option that mix a user-supplied
string into every content-addressed hash used to generate chunk
filenames and static asset filenames. This works for both Webpack and
Turbopack.

When both are set, the values are concatenated (`outputHashSalt +
NEXT_HASH_SALT`), so a per-project salt can be baked into
`next.config.js` while a per-deployment salt is injected at build time
via the environment variable.

### Why?

Content-addressed filenames (e.g. `chunk.abc123.js`) are derived from
file content, so they only change when the content changes. There are
deployment scenarios where you need to force all filenames to rotate —
for example after a CDN misconfiguration has poisoned caches for a
particular hash space — without actually changing source code. A stable,
opt-in salt lets operators do this without touching application code.

Some customers prefer the config-file approach
(`turbopack.outputHashSalt`) over environment variables, so both are
supported.

### How?

**Webpack** already has `output.hashSalt` in its config. We simply
forward `NEXT_HASH_SALT` to that option.

**Turbopack** required threading the value through several layers:

1. The effective hash salt is computed once in
`assignDefaultsAndValidate` as `config.turbopackHashSalt =
(turbopack.outputHashSalt ?? '') + (NEXT_HASH_SALT ?? '')` and stored on
`NextConfigComplete`. Both `turbopackBuild` (production) and
`createHotReloaderTurbopack` (dev) read from this single field.
2. `ProjectOptions.hash_salt` receives the pre-computed salt.
3. `Project` stores the salt and passes it into the three chunking
context option structs (`ClientChunkingContextOptions`,
`ServerChunkingContextOptions`, `EdgeChunkingContextOptions`).
4. Both `BrowserChunkingContext` and `NodeJsChunkingContext` gain a
`hash_salt: RcStr` field.
5. A new `deterministic_hash_with_salt(salt, input, algorithm)` function
in `turbo-tasks-hash` writes the salt bytes first, then the content
bytes, into a single hasher — one pass, no hash-of-hash composition.
6. A matching `content_hash_with_salt` method is added to `FileContent`
and `AssetContent`.
7. `ChunkingContext::asset_path` is changed to accept `Vc<AssetContent>`
(instead of a pre-computed `Vc<RcStr>`) so the chunking context can
choose the correct hash path itself. `StaticOutputAsset::path`
simplifies accordingly.

Without `NEXT_HASH_SALT` and without `turbopack.outputHashSalt` set,
behaviour is identical to before — no hash change, no performance
impact.

**e2e test** (`test/production/app-dir/hash-salt/`) verifies:
- Two builds with the same salt produce identical chunk and static asset
filenames.
- A build with a different salt produces different filenames.
- `turbopack.outputHashSalt` (config) changes filenames vs no salt.
- Combined config + env salt differs from either alone.
- Runs for both Turbopack and Webpack.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
2026-04-01 22:03:50 +02:00
Niklas Mischkulnig feb594bd3c Turbopack: use same hash for immutable-static-hashes.json (#92157) 2026-03-31 19:17:32 +02:00
Hendrik Liebau 2aabb6de35 Revert "turbo-tasks: add hashed cell mode for hash-based change detection without cell data" (#92103)
Reverts vercel/next.js#91576

That PR leads to failures in
`test/e2e/filesystem-cache/filesystem-cache.test.ts`, e.g.:

```
FATAL: An unexpected Turbopack error occurred:
Failed to write app endpoint /page

Caused by:
- failed to analyze ecmascript module '[project]/node_modules/next/dist/esm/build/templates/app-page.js?page=/remove-me/page { MODULE_0 => "[project]/app/layout.tsx [app-rsc] (ecmascript, Next.js Server Component)", MODULE_1 => "[project]/node_modules/next/dist/client/components/builtin/not-found.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_2 => "[project]/node_modules/next/dist/client/components/builtin/forbidden.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_3 => "[project]/node_modules/next/dist/client/components/builtin/unauthorized.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_4 => "[project]/node_modules/next/dist/client/components/builtin/global-error.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_5 => "[project]/app/remove-me/page.tsx [app-rsc] (ecmascript, Next.js Server Component)" } [app-rsc] (ecmascript)'
- failed to parse [project]/node_modules/next/dist/esm/build/templates/app-page.js?page=/remove-me/page
- Cell CellId { type_id: ValueTypeId { id: 234, name: ValueType { name: "turbo_tasks_fs::FileContent" } }, index: 1 } no longer exists in task TaskId { id: 793 } get_app_page_entry (no cell of this type exists)

Debug info:
- Execution of get_all_written_entrypoints_with_issues_operation failed
- Execution of EntrypointsOperation::new failed
- Execution of all_entrypoints_write_to_disk_operation failed
- Execution of output_assets_operation failed
- Execution of <AppEndpoint as Endpoint>::output failed
- Failed to write app endpoint /page
- Execution of AppEndpoint::output failed
- Execution of whole_app_module_graph_operation failed
- Execution of Project::get_all_entries failed
- Execution of <AppEndpoint as Endpoint>::entries failed
- Execution of get_app_page_entry failed
- Execution of *ProcessResult::module failed
- Execution of <ModuleAssetContext as AssetContext>::process failed
- Execution of *EcmascriptExports::split_locals_and_reexports failed
- Execution of <EcmascriptModuleAsset as EcmascriptChunkPlaceable>::get_exports failed
- Execution of analyze_ecmascript_module failed
- failed to analyze ecmascript module '[project]/node_modules/next/dist/esm/build/templates/app-page.js?page=/remove-me/page { MODULE_0 => "[project]/app/layout.tsx [app-rsc] (ecmascript, Next.js Server Component)", MODULE_1 => "[project]/node_modules/next/dist/client/components/builtin/not-found.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_2 => "[project]/node_modules/next/dist/client/components/builtin/forbidden.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_3 => "[project]/node_modules/next/dist/client/components/builtin/unauthorized.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_4 => "[project]/node_modules/next/dist/client/components/builtin/global-error.js [app-rsc] (ecmascript, Next.js Server Component)", MODULE_5 => "[project]/app/remove-me/page.tsx [app-rsc] (ecmascript, Next.js Server Component)" } [app-rsc] (ecmascript)'
- Execution of parse failed
- failed to parse [project]/node_modules/next/dist/esm/build/templates/app-page.js?page=/remove-me/page
- Cell CellId { type_id: ValueTypeId { id: 234, name: ValueType { name: "turbo_tasks_fs::FileContent" } }, index: 1 } no longer exists in task TaskId { id: 793 } get_app_page_entry (no cell of this type exists)
```


[x-ref](https://github.com/vercel/next.js/actions/runs/23707462785/job/69156136747?pr=91830)
2026-03-30 14:34:37 +00:00
Tobias Koppers d84e3bb56e Use RcStr directly in napi(object) structs instead of converting to String (#92014)
### What?

Replace `String` fields in `#[napi(object)]` structs within
`crates/next-napi-bindings/` with `RcStr`, and remove the corresponding
unnecessary `.to_string()` / `.into_owned()` conversions at the NAPI
boundary.

Affected structs and fields:

| File | Struct | Fields |
|---|---|---|
| `utils.rs` | `NapiIssue` | `file_path`, `documentation_link` |
| `utils.rs` | `NapiAdditionalIssueSource` | `description` |
| `utils.rs` | `NapiSource` | `ident`, `file_path` |
| `utils.rs` | `NapiDiagnostic` | `category`, `name`, `payload` |
| `project.rs` | `NapiRoute` | `pathname` |
| `endpoint.rs` | `NapiAssetPath` | `path`, `content_hash` |

### Why?

`RcStr` already implements `ToNapiValue` and `FromNapiValue` (behind the
`napi` feature flag in `turbo-rcstr`, already enabled in
`next-napi-bindings`). Despite this, many `#[napi(object)]` structs were
still declared with `String` fields and converted from `RcStr` source
data via `.to_string()` or `.into_owned()`, allocating a new heap
`String` on every call just to hand it straight to the NAPI serializer.

Since `RcStr` is reference-counted, using it directly avoids the
allocation entirely — the refcount is simply incremented on clone and
the underlying bytes are passed to `napi_create_string_utf8` directly.

### How?

- Changed `String` → `RcStr` in the struct field declarations.
- Updated the corresponding `From` / constructor impls to use `.clone()`
instead of `.to_string()` / `.into_owned()` on `RcStr` values, and
direct field moves where ownership is transferred.
- Added `use turbo_rcstr::RcStr;` imports where not already present
(`utils.rs`, `endpoint.rs`). `project.rs` already imported `RcStr`.
- The JavaScript API surface is unchanged: both `String` and `RcStr`
serialize to a JS `string` via the same underlying
`napi_create_string_utf8` call.

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-28 14:18:14 -07:00
Tobias Koppers 0090db224d turbo-tasks: add hashed cell mode for hash-based change detection without cell data (#91576)
## Summary

### Core: hash-based cell change detection

- Adds `cell_data_hash: AutoMap<CellId, [u8; 16]>` to the storage schema
(`category = "data"`) so the backend can persist a hash of transient
cell data across evictions
- Stored as `[u8; 16]` (little-endian bytes of a u128) rather than
`u128` to keep the 1-byte alignment out of `AutoMap` and therefore out
of the `LazyField` enum — a bare `u128` would grow the enum from 56 to
64 bytes due to its 16-byte alignment requirement
- Adds `content_hash: Option<u128>` to `UpdateCellOperation::run` and
threads it through the full call chain: `CurrentCellRef` →
`TurboTasksCallApi::update_own_task_cell` → `Backend::update_task_cell`
→ operation
- **New invalidation logic** in `UpdateCellOperation::run` for the
`assume_unchanged = false` path:
  - Old content available → real equality compare (unchanged)
- Old content evicted, hashes match → write new content but **skip
dependent invalidation**
- Old content evicted, hashes differ (or missing) → write + invalidate
as before
- `cell_data_hash` is updated whenever content is written (skipped if
hash is unchanged; always updated for non-serializable cells regardless
of `assume_unchanged`)
- Adds `hashed_compare_and_update` /
`hashed_compare_and_update_with_shared_reference` methods to
`CurrentCellRef` (require `T: PartialEq + DeterministicHash`); hash is
computed lazily (only after equality check fails when old value is
available)
- Adds `VcCellHashedCompareMode<T>` in `cell_mode.rs` which implements
`VcCellMode<T>` for `T: VcValueType + PartialEq + DeterministicHash`

### Macro: `serialization = "hash"` and `hash = "manual"`

- **`serialization = "hash"`** — behaves like `serialization = "none"`
(no disk serialization, transient) but uses `VcCellHashedCompareMode` so
the stored hash prevents spurious downstream invalidation when transient
data is evicted and re-executed
- Valid only with `cell = "compare"` (the default); combining with `cell
= "new"` or `cell = "keyed"` is a compile error
  - Automatically derives `DeterministicHash` on the annotated type
- **`hash = "manual"`** — opt-out of the auto-derive when a custom
`DeterministicHash` impl is needed (analogous to `eq = "manual"`); using
`hash = "manual"` without `serialization = "hash"` is a compile error

### turbo-tasks-fs: `PersistedFileContent`

- Adds `PersistedFileContent` — a mirror of `FileContent` that is
returned by `Vc<FileContent>::persist()`, storing the same data but
obtained through the persistent cache path
- `FileContent` is switched to `serialization = "hash"` so the macro
auto-derives `DeterministicHash`
- `DiskFileSystem::write()` now calls `.persist().await?` before
emitting the write effect, using `PersistedFileContent` for the file
comparison and write — this ensures full content is in the persistent
cache on restore, avoiding spurious downstream invalidation
- `WriteContent::File` in `invalidator_map.rs` is updated to hold
`ReadRef<PersistedFileContent>`

### turbopack-core: `Code` with hash-based serialization

- Adds `DeterministicHash` impls for `SmallVec<[T; N]>` and `()` in
`turbo-tasks-hash`
- Switches `Code` to `serialization = "hash"` so module factory code
cells use hash-based change detection
- Calls `.persist()` on module factory code cells in
`turbopack-ecmascript`

### next-core: duplicate asset detection and parallel emit

- `emit_assets` now emits node and client assets concurrently via
`try_join!`
- Detects duplicate assets (same output path, different content) and
returns a hard error with a diff summary — previously the last writer
silently won

## Macro syntax

```rust
#[turbo_tasks::value(serialization = "hash")]
struct MyTransientType { ... }

// opt out of auto-derived DeterministicHash when you need a custom impl:
#[turbo_tasks::value(serialization = "hash", hash = "manual")]
struct MyTransientType { ... }
```

## Test plan

- [x] Integration tests in
`turbopack/crates/turbo-tasks-backend/tests/hashed_cell_mode.rs`:
- `test_hashed_cell_mode_change_triggers_invalidation` — value change
triggers consumer re-execution
- `test_hashed_cell_mode_equal_value_no_invalidation` — same hash
prevents consumer re-execution
- [x] `cargo test -p turbo-tasks-macros-tests` passes
- [x] `cargo test -p turbo-tasks-backend --test hashed_cell_mode` passes
- [x] `cargo check --workspace` passes

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2026-03-26 12:32:45 -07:00
Tobias Koppers 548fbca6df turbopack: Remove Vc::resolve(), migrate all callsites to Vc::to_resolved() (#91725)
### What?

Remove the `Vc::resolve()` method and migrate all ~37 callsites across
the turbopack and next crates to use `Vc::to_resolved()` instead.

### Why?

`Vc::resolve()` returned `Result<Vc<T>>` — a resolved `Vc` with no
static guarantee that it is resolved. This is a footgun: the type is
indistinguishable from any other unresolved `Vc<T>`, so it's easy to
accidentally lose the "resolved" property at a type boundary or store it
in a struct without the compiler enforcing invariants.

`Vc::to_resolved()` returns `Result<ResolvedVc<T>>`, which statically
encodes the resolved guarantee in the type. `ResolvedVc<T>` implements
`Deref<Target = Vc<T>>`, so it coerces seamlessly to `Vc<T>` at call
sites that still need the plain type. Removing `Vc::resolve()`
eliminates this footgun entirely and pushes callers toward the type-safe
API.

### How?

- Removed the `Vc::resolve()` method from `turbo-tasks/src/vc/mod.rs`.
- Updated `Vc::to_resolved()` to construct the inner `Vc` directly via
`self.node.resolve()` rather than calling `self.resolve()`.
- Updated `Vc::debug_identifier` to go through `to_resolved()`.
- Updated `TaskInput::resolve_input` for `Vc<T>` to use `to_resolved()`.
- Updated all callsites across turbopack-core, turbopack-ecmascript,
turbopack-resolve, turbopack-browser, turbopack-css, turbopack-node,
turbopack-ecmascript-runtime, next-api, next-core, and test/fuzz files:
- Where `ResolvedVc<T>` is usable (deref coercion handles method calls,
or the value is stored in a `HashMap` etc.), simply replaced
`.resolve()` with `.to_resolved()`.
- Where a plain `Vc<T>` is strictly required by a callee signature,
dereferenced with `*x.to_resolved().await?`.
- Updated the `AfterResolvePluginWithCondition` type alias in
`resolve/mod.rs` to use `ResolvedVc<AfterResolvePluginCondition>` now
that the tuple member is produced by `to_resolved()`.
- Updated doc comments referencing `Vc::resolve` to point to
`Vc::to_resolved`.

Verified with `cargo check --all-targets` — compiles cleanly with no
errors.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-26 11:30:36 -07:00
Tobias Koppers 43f9017719 Add next internal post-build CLI command for Turbopack database compaction (#91336)
### What?

Adds a \`next internal post-build\` CLI command that performs Turbopack
persistent cache database compaction as a separate step after \`next
build\`.

### Why?

Database compaction during shutdown can be slow and blocks the build
process. By deferring compaction to a separate post-build step, we can:
- Keep the main \`next build\` fast by skipping compaction at shutdown
(controlled via \`NEXT_USE_POST_BUILD=1\`)
- Run compaction independently, e.g. as a separate CI step or in the
background
- Perform a full compaction (\`max_merge_segment_count: usize::MAX\`)
that is more thorough than the runtime shutdown compaction

**Intended usage in a build pipeline:**
\`\`\`sh
NEXT_USE_POST_BUILD=1 next build # fast build, skips compaction at
shutdown
next internal post-build # separate step: full compaction of the cache
\`\`\`

### How?

**Rust side:**
- Added \`compact_database()\` to \`turbo-tasks-backend\` that opens the
database and runs a full compaction using
\`TurboTasksParallelScheduler\` backed by a dedicated multi-threaded
Tokio runtime (same scheduler as the normal runtime path, enabling
parallel work-stealing)
- Added \`turbopack_database_compact\` NAPI binding that resolves the
versioned DB path and calls \`compact_database()\`
- Added \`skip_compaction\` flag to \`NapiTurboEngineOptions\` /
\`TurboKeyValueDatabase\` to skip compaction during normal shutdown when
post-build will handle it; asserted that \`skip_compaction=true\`
requires \`is_short_session=true\` (enforcing the logical dependency)
- Extracted shared \`DB_CONFIG\`, \`FAMILIES\`, and \`COMPACT_CONFIG\`
constants and \`git_version_info()\` helper to avoid duplication between
the runtime DB and standalone compaction paths

**TypeScript side:**
- Added \`next internal post-build\` command that loads the Next.js
config, finds the Turbopack cache directory, and calls the native
compaction binding
- Added \`databaseCompact\` to the turbo bindings interface (native +
WASM stub)
- \`turbopack-build/impl.ts\` sets \`skipCompaction: true\` when
\`NEXT_USE_POST_BUILD=1\`

**Tests:**
- Added \`test/production/app-dir/post-build/\` integration test that
verifies compaction runs successfully on a persistent-cache build and
reports nothing to do when persistent caching is disabled

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-24 13:34:35 -07:00
Tobias Koppers 7d451fe01e Add module count field to module graph tracing spans (#91697)
### What?

Adds a `modules` field to two tracing spans that cover module graph
construction:

- `"module graph for endpoint"` span (per-page graph, in `app.rs`) —
records the total number of modules across all `SingleModuleGraph`s
built for that endpoint.
- `"whole app module graph"` span (new info-level span wrapping
`whole_app_module_graph_operation` in `project.rs`) — records the
combined module count of the base graph and the additional-entries
graph.

Also adds a `module_count()` method to `SingleModuleGraph` (as a
`#[turbo_tasks::function]` returning `Vc<u64>`) that exposes
`number_of_modules`.

### Why?

Module graph size directly affects build and HMR performance. Without a
count in the trace, it is impossible to correlate span duration with
graph size or spot regressions where the graph grows unexpectedly.
Surfacing the count as a structured span field makes it queryable
without requiring log parsing.

### How?

- `SingleModuleGraph::module_count()` is a cheap
`#[turbo_tasks::function]` that wraps the already-computed
`number_of_modules: usize` field as a `Vc<u64>`.
- Both call sites follow the deferred-field pattern already used in
`ProjectContainer::initialize`: the span is created with `modules =
Empty` before the `async move` block, `span.record("modules", count)` is
called once the graphs are ready, and `span_clone` is passed to
`.instrument()` so the field is recorded on the correct span.
- Module counts are summed across all graphs that contribute to the
endpoint/app.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2026-03-20 19:14:48 +01:00
Niklas Mischkulnig 118bdaf68c Turbopack: lazy require metadata and handle TLA (#91705)
A regression from #88487

1. Make metadata import lazy with `() => require()`, just like for the layout segments
2. Properly await the return value to better handle TLA modules

This align with Webpack which does this:

<img width="1535" height="509" alt="Bildschirmfoto 2026-03-20 um 11 58 00" src="https://github.com/user-attachments/assets/f2864c86-ccce-4884-8417-c8ae06c05f78" />


Closes PACK-6927
Closes #91700
Closes https://github.com/vercel/next.js/issues/91676
2026-03-20 16:30:32 +01:00
Tobias Koppers 6a8a31a7f4 Fix layout segment optimization: move app-page imports to server-utility transition (#91701)
## Summary

- Add `'turbopack-transition': 'next-server-utility'` to all server-side imports in the `app-page.ts` template so they are properly placed in the server utility layer, rather than relying on `SharedMerged` chunk groups to handle them
- Remove the now-unnecessary `next-server-utility` transition from `fillMetadataSegment` import in `app_page_loader_tree.rs`
- Add `SharedMultiple` chunk group variant to support multiple shared entries without a merge tag
  - This avoid passing the `parent` into `SharedMerged`, which did break the layout segment optimization.
- Refactor `app_module_graphs` to combine `client_shared_entries` + `has_layout_segments` into a single `Option` parameter, simplifying the API and removing the dead code path for non-page endpoints

## Test plan

- [ ] Verify dev and production builds work with app pages that have layout segments
- [ ] Verify route handlers still build correctly (no client runtime entries passed)
- [ ] Run existing app-dir e2e tests
2026-03-20 13:00:43 +01:00
Will Binns-Smith 62393b0402 Turbopack: enable server HMR for app route handlers (#91466)
App router handlers are already built with the Turbopack runtime, so
they can use server HMR. **We also already get subscription events for
their chunks.** However, unlike app pages which use
`__next_app__.require()` for dynamic devModuleCache lookup, route
handlers capture userland exports statically in AppRouteRouteModule at
construction time. This change makes routes behave a lot like pages,
dynamically loading the user's code when requests are made. This way, we
can freely invalidate and reload it when changes are made without
evicting the entire require cache.

Only `next dev` uses this lazy evaluation, as `next start` continues to
eagerly import route handlers.

This change removes the `isAppPage` restriction from `usesServerHmr`,
extending server HMR coverage to all App Router entries (pages and route
handlers) built with the Node.js runtime.

For route handlers, we also clear the entry chunk from Node.js
`require.cache` on each rebuild so the next `requirePage()` call
re-executes the entry and obtains fresh module exports from
`devModuleCache` (which HMR updates in-place).

Test Plan: Added an additional e2e test, `with-dep`
2026-03-19 03:57:41 +00:00
Josh Story 0ca967b863 Add group depth tracking to instant validation boundary discovery (#91208)
The depth-based instant validation previously only iterated
URL-consuming segments as potential shared/new boundaries. Route groups
between URL segments were invisible — when a route group layout with
Suspense was shared in a client navigation, its Suspense appeared to
cover blocking code in the validation render even though it wouldn't in
reality.

This adds a second dimension to the validation iteration: group depth.
At each URL depth, we now also step through route group boundaries. A
new `discoverValidationDepths` function walks the LoaderTree and returns
an array where each entry represents the max group depth at that URL
depth. The validation loop iterates from deepest group depth to
shallowest within each URL depth, stopping on the first error.

Key design decisions:
- URL depth and group depth counters are advanced before the boundary
check, making the boundary condition a simple `nextUrlDepth > depth &&
currentGroupDepth >= groupDepth`
- The synthetic `(slot)` segment that Next.js inserts for parallel slots
is excluded from group depth counting since it doesn't represent a real
navigation boundary
- Group depths are discovered from the LoaderTree rather than the URL
pathname, making the tree the source of truth for validation bounds
2026-03-17 22:14:19 -07:00
Tobias Koppers ee1f9d79ad Remove webpack stats generation from Turbopack (#91515)
### What?

Remove the `TURBOPACK_STATS` feature that generated webpack-compatible
`webpack-stats.json` files from Turbopack's output.

**Deleted:**
- `crates/next-api/src/webpack_stats.rs` — the entire 305-line Rust
implementation that built webpack-format stats (assets, chunks, modules,
entrypoints) from Turbopack's module graph

**Rust changes:**
- `crates/next-api/src/lib.rs` — removed `mod webpack_stats;`
- `crates/next-api/src/project.rs` — removed
`should_create_webpack_stats()` which read the `TURBOPACK_STATS` env var
- `crates/next-api/src/pages.rs` — removed `generate_webpack_stats`
import and stats output emission
- `crates/next-api/src/app.rs` — same

**TypeScript changes:**
- `packages/next/src/shared/lib/constants.ts` — removed `WEBPACK_STATS =
'webpack-stats.json'` constant
- `packages/next/src/shared/lib/turbopack/manifest-loader.ts` — removed
`loadWebpackStats()`, `writeWebpackStats()`, `mergeWebpackStats()`, the
`webpackStats` field, webpack type imports, and the `TURBOPACK_STATS`
env guard in `writeManifests()`
- `packages/next/src/server/dev/turbopack-utils.ts` — removed
`shouldCreateWebpackStats` flag and two `loadWebpackStats()` call sites
- `packages/next/src/build/handle-entrypoints.ts` — same

### Why?

This feature is no longer needed and adds dead complexity. It was opt-in
via `TURBOPACK_STATS=1` but nothing in the current codebase or tooling
depends on it.

### How?

Pure deletion — no behavior change for any existing user, as the feature
was opt-in via an environment variable that is no longer checked.

**Intentionally left untouched:**
- Webpack's own `stats.json` generation (non-Turbopack path)
- The Turbopack bundle analyzer / `analyze` feature
- `route-bundle-stats.json` in `.next/diagnostics/`

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-17 13:58:51 -07:00
Jiwon Choi 66478522a8 Improve RSC error messages (#90702)
Improve the RSC error messages to be more comprehensive.

x-ref:
https://github.com/vercel/next.js/pull/89688#discussion_r2865847895

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-17 14:37:26 +01:00
Benjamin Woodruff 924d39dcef docs(turbopack): Fix all warnings in cargo doc --no-deps --workspace, and run rustdoc as part of our CI checks (#91339)
This gets `cargo doc` clean, and enables it in CI so that we won't regress it (I'm open to removing this if it proves to be annoying).

I also cleaned up some documentation and even found some dead code (deleted `turbopack/crates/turbopack-dev-server/src/source/conditional.rs`).
2026-03-16 16:42:24 -07:00
Will Binns-Smith 0f867bb219 Turbopack: Enable server HMR by default for app pages (#91476)
This enables server HMR (formally, this is called server fast refresh)
by default for app router pages. For now, it's still disabled for routes
and middleware even with app router.

- Replaces `--experimental-server-fast-refresh` with
`--no-server-fast-refresh` to opt out
- Updates telemetry to record `feature.serverFastRefreshDisabled` when
the disable flag is passed

- [x] Verify `next dev` enables server HMR by default (Turbopack, app
dir, non-edge routes)
- [x] Verify `next dev --no-server-fast-refresh` disables it
- CI
2026-03-16 16:37:52 -07:00
Jiwon Choi fa32daad8d Add unstable_catchError() API for custom error boundary (#89688)
This PR adds `unstable_catchError()` API for a granular custom error
boundary. The error component generated by this API is not a true
component format, as the wrapper provides additional args. Therefore,
the API is a function call by design rather than a boundary component to
avoid merging the `errorInfo` with the user-provided props from the
wrapper.

This API works for both the App/Pages Router. However, `retry()` is not
allowed in Pages Router as it depends on `router.refresh()`, and will
throw. Also, it's exported from `next/error` as there was already an
`Error` component for the [Pages
Router](https://nextjs.org/docs/pages/building-your-application/routing/custom-error#reusing-the-built-in-error-page).

### DevTools

<img width="508" height="90" alt="CleanShot 2026-03-13 at 03 02 46@2x"
src="https://github.com/user-attachments/assets/8b79b1f6-877d-4901-99f7-6a2b4d3e34fe"
/>

Docs to follow up: https://github.com/vercel/next.js/pull/89847
Closes NAR-768

---------

Co-authored-by: Josh Story <story@hey.com>
2026-03-16 22:57:11 +00:00
Niklas Mischkulnig 46789444d5 Remove unused layer from server actions manifest (#91425)
```
    // This is only used by Webpack to correctly output the manifest. It's value shouldn't be relied
    // upon externally. It's possible that the same action can be in different layers in a single
    // page, which cannot be modelled with this API anyway.
```
2026-03-16 14:10:22 +01:00
Tobias Koppers 261922df82 Show generated code from loaders in parse error messages (#89898)
## What?

When a webpack/turbopack loader produces broken code, error messages now
display **both** the original source and the generated code with source
map information, making it much easier to debug loader issues.

## Why?

Previously, when loaders returned invalid code, error messages only
showed the original source file (after source-map remapping). Users had
no way to see what the loader actually generated, making it hard to
diagnose why the code failed to parse. Showing both sides gives full
context about what went wrong.

## How?

### Turbopack Core (`turbopack-core`)

- **`Source::description()`** — New method on the `Source` trait
providing human-readable descriptions of where code comes from.
Implemented across all source types (`FileSource`, `VirtualSource`,
`WebpackLoadersProcessedAsset`, `PostCssTransformedAsset`, etc.),
producing chains like `"loaders [sass-loader] transform of file content
of ./styles.scss"`.
- **`AdditionalIssueSource`** — New struct to hold a labeled source
location. The `Issue` trait gains an `additional_sources()` method so
issues can expose supplementary code frames.
- **`GeneratedCodeSource`** — A wrapper that strips `GenerateSourceMap`
support from a source, ensuring the *generated* code is displayed as-is
rather than being remapped back to the original.
- **`IssueSource::to_generated_code_source()`** — Helper that detects
sources implementing `GenerateSourceMap` and wraps them in
`GeneratedCodeSource` for display. Used by `AnalyzeIssue` and
`ParsingIssue` to automatically attach generated code frames.

### Error Formatting

- **`turbopack-cli-utils`** — Renders additional sources in CLI issue
output.
- **`format-issue.ts`** — Renders additional sources in the browser
error overlay. Extracted `formatSourceCodeFrame()` helper to deduplicate
code-frame rendering between primary and additional sources.
- Long-line truncation (e.g. minified CSS from SCSS) is handled natively
by the Rust-based `codeFrameColumns` implementation.

### Type Definitions

- Added `SourcePosition`, `IssueSource`, and `AdditionalIssueSource`
interfaces to TypeScript types.
- Updated `PlainSource` (added `file_path`), `PlainIssue` (added
`additional_sources`), and NAPI bindings to pass the data through.

### Test Coverage

- **E2e tests** (`test/e2e/webpack-loader-parse-error/`) with custom
broken JS and CSS loaders, covering all 4 modes:
- **Development (Turbopack)** — Verifies parse errors show both original
and generated code via browser error overlay
- **Development (Webpack)** — Verifies error overlay shows the parse
error (webpack doesn't support additional sources)
- **Production (Turbopack)** — Verifies build failure output with full
error extraction and inline snapshots
- **Production (Webpack)** — Verifies build failure output with inline
snapshots
- Updated `test/development/sass-error/` snapshot to include the new
generated code frame for minified SCSS output.

### Example Output

When a loader produces broken code, users now see:
```
⨯ ./app/data.broken.js:3:1
Parsing ecmascript source code failed
  1 | // This file will be processed by broken-js-loader
  2 | // The loader will return invalid JavaScript with a source map
> 3 | export default function Data() {
    | ^
  4 |   return <div>original source content</div>
  5 | }
  6 |

Expected '</', got '{'

Generated code of loaders [./broken-js-loader.js] transform of file content of app/data.broken.js:
./app/data.broken.js:3:46
  1 | // Generated by broken-js-loader
  2 | export default function Page() {
> 3 |   return <div>this is intentionally broken {{{ invalid jsx
    |                                              ^
  4 | }
  5 |

Import trace:
  Server Component:
    ./app/data.broken.js
    ./app/page.js
```

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <lukeisandberg@gmail.com>
2026-03-16 13:41:29 +01:00
Niklas Mischkulnig d9751eaa14 Turbopack: use ChunkGroupEntry::Shared, part 2 (#91279)
Followup to https://github.com/vercel/next.js/pull/90978, which fixed the `ChunkGroup`s  passed to `.chunk_group()`, forgot to update the the module graph entry types, which didn't match up.

The `ChunkGroupEntry::Shared(ResolvedVc::upcast(*server_component))` one was more involved:
- We used to have `layout.js [app-rsc] (Next.js Server Component)` --ChunkingType::Shared--> `layout.js [app-rsc]` (the actual module)
- But this meant that the actual chunk group was `Shared(server_component.await?.module)` (the inner one)
- This wasn't possible to use for the module graph construction though, as the client reference transform needs to see the `layout.js [app-rsc] (Next.js Server Component)` module during the traversal, to infer the parent server component of the given subgraph.

So instead, move to
- `loader-tree in the template`
- --ChunkingType::Shared-->
- `layout.js [app-rsc] (Next.js Server Component)`
- --ChunkingType::Parallel-->
- `layout.js [app-rsc]`

The reason for that `ChunkingType::Shared` is so that we can chunk arbitrary modules that are loaded by `ChunkGroup::Entry`. Meaning that we can do `chunk_group(Shared(root_layout))).concat(chunk_group(Shared(layout))).concat(chunk_group(Entry(page)))` and rely on turbotask caching to deduplicate layout chunking across pages.
2026-03-16 12:02:31 +01:00
Luke Sandberg 2e4f08ca0a fix(build): don't block SSG on telemetry flush, add persistence spans to trace-build (#91335)
### What?

Two fixes for the Turbopack build tracing introduced in #90397:

1. **Don't block SSG on Turbopack shutdown**: `workerMain()` no longer awaits the shutdown promise before returning. Trace event collection is deferred to `waitForShutdown()`, which the parent process awaits *after* SSG completes. This allows static generation and Turbopack persistence/cache-flush to run in parallel.

2. **Add persistence spans to `trace-build` allowlist**: `turbopack-build-events`, `turbopack-persistence`, and `turbopack-compaction` are now included in the `to-json-build.ts` allowlist so they appear in `.next/trace-build`.

### Why?

- The `await shutdownPromise` in `workerMain()` was too eager — it prevented the caller from acknowledging the build as complete and starting SSG until Turbopack persistence finished flushing to disk.
- The persistence/compaction spans emitted by Rust (`turbopack-persistence`, `turbopack-compaction`) were not in the `to-json-build.ts` allowlist, so they were silently filtered out of `.next/trace-build`.

### How?

**`impl.ts` (worker)**:
- Removed `await shutdownPromise` from `workerMain()` — it now returns build results immediately
- `waitForShutdown()` now returns `{ debugTraceEvents }` after awaiting shutdown, so trace events are collected only after all compilation events (including persistence spans) have been processed

**`index.ts` (parent)**:
- Moved `recordTraceEvents(debugTraceEvents)` from the `workerMain` result handler into the `shutdownPromise` `.then()` chain, so events are replayed into the parent reporter after shutdown completes

**`to-json-build.ts`**:
- Added `turbopack-build-events`, `turbopack-persistence`, `turbopack-compaction` to the allowlist

**Test updates**:
- Enabled `turbopackFileSystemCacheForBuild: true` in the trace-build test fixture
- Updated the Turbopack inline snapshot to include `turbopack-build-events`
2026-03-15 23:42:17 -07:00
Matt Mastracci 2b4bde2ebb Enable thin LTO for release builds (#91343)
# What

Add `lto = "thin"` to the workspace-level `[profile.release]` in
`Cargo.toml`.

# Why

Thin LTO enables cross-crate optimization at link time, improving
runtime
performance of the final artifacts (next-napi-bindings native module,
turbopack-cli binary, and benchmarks) without the full build time cost
of
fat LTO.

# How

Added `lto = "thin"` to the workspace root `[profile.release]` section.
This applies to all release-profile link targets in the workspace,
including
binaries, cdylibs, and benchmarks. The `release-with-assertions` and
`release-with-debug` profiles inherit from `release`, so they also
benefit.
2026-03-14 12:49:43 -06:00
Niklas Mischkulnig 202db657d8 Turbopack: don't emit polyfill chunk for API routes (#91331)
1. This is misldeading in the bundle analyzer view
2. If you have a Nextjs app consisting of only API routes, then there is
no need to ever write out any polyfill chunk.
2026-03-13 13:58:16 -07:00
Tobias Koppers e22988e5ee Turbopack: switch chunk/asset hashes from hex to base40 encoding (#91137)
### What?

Switch Turbopack's hash encoding for chunk and asset output filenames
from hexadecimal (base16) to base40, using the alphabet \`0-9 a-z _ - ~
.\`. Version hashes (used for HMR update comparison, not filenames) use
base64 instead.

### Why?

Base40 encodes the same number of bits in fewer characters than hex,
producing shorter output filenames. All 40 characters are RFC 3986
unreserved (URL-safe) and safe on case-insensitive filesystems (macOS
HFS+/APFS, Windows NTFS).

Hash truncation lengths are reduced proportionally to maintain
equivalent collision resistance:

| Context | Before (hex) | After (base40) | Entropy |
|---|---|---|---|
| Content hash in chunk filenames | 16 chars | 13 chars | ~69 bits |
| Content hash in asset filenames | 8 chars | 13 chars | ~69 bits |
| Ident disambiguator hash | 8 chars | 7 chars | ~37 bits |
| Long-path prefix hash | 5 chars | 4 chars | ~21 bits |

### How?

**New encoding module** (\`turbo-tasks-hash/src/base40.rs\`):
- Defines the base40 alphabet and length constants (\`BASE40_LEN_64 =
13\`, \`BASE40_LEN_128 = 25\`)
- Implements a generic \`encode_base40_fixed<N>\` helper to avoid
duplication
- Public API: \`encode_base40(u64) -> String\` and
\`encode_base40_128(u128) -> String\`

**New base64 encoding** (\`turbo-tasks-hash/src/base64.rs\`):
- \`encode_base64(u64) -> String\` — 11-char base64 (no padding) for
version hashes
- Version hashes don't appear in URLs or filenames, so base64 is safe
and shorter

**New \`HashAlgorithm\` variants** (\`turbo-tasks-hash/src/lib.rs\`):
- \`Xxh3Hash64Base40\` and \`Xxh3Hash128Base40\` added alongside
existing hex variants
- Existing hex variants kept for internal manifests and identifiers

**\`ContentHashing\` moved to \`turbopack-core\`**:
- Moved from \`turbopack-browser\` to
\`turbopack-core/src/chunk/mod.rs\` so both \`BrowserChunkingContext\`
and \`NodeJsChunkingContext\` can use it

**Separate chunk vs asset content hashing**:
- \`BrowserChunkingContext\`: \`content_hashing\` renamed to
\`chunk_content_hashing\` (optional), new \`asset_content_hashing:
ContentHashing\` field (non-optional, defaults to 13 chars)
- \`NodeJsChunkingContext\`: new \`asset_content_hashing:
ContentHashing\` field (non-optional, defaults to 13 chars)
- Builder methods: \`use_content_hashing()\` renamed to
\`chunk_content_hashing()\`, new \`asset_content_hashing()\`

**Version hashes switched to base64**:
- \`turbopack-nodejs/src/ecmascript/node/version.rs\`
- \`turbopack-dev-server/src/html.rs\`
- \`turbopack-browser/src/ecmascript/version.rs\`,
\`merged/version.rs\`, \`list/version.rs\`

**Other callers updated** (15 files across turbopack and next-core):
- All chunk/asset content hashing switched from \`Xxh3Hash128Hex\` →
\`Xxh3Hash128Base40\`
- \`ContentHashing::Direct { length }\` reduced from 16 → 13
- Asset path truncations use full 13-char base40 hash (matching chunk
filenames)

**Exception — \`wasm_edge_var_name\`** (\`turbopack-wasm/src/lib.rs\`):
- Kept as \`Xxh3Hash128Hex\` because the hash is used as part of a
JavaScript variable name (\`wasm_{hash}\`), and base40 characters \`-\`,
\`~\`, \`.\` are not valid JS identifier characters.

**Scope — NOT changed:**
- Webpack configuration (unchanged)
- Internal manifests (\`routes_hashes_manifest\`,
\`project_asset_hashes_manifest\`)
- Internal identifiers (font naming, external module hashing, data URI
sources, debug IDs)
- SRI hashes (SHA-based Base64, different purpose)

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 20:38:02 +00:00
Luke Sandberg 236a76dd0f [turbopack] Remove turbo_tasks::function from ModuleReference getters (#91229)
### What?

Refactors the `ModuleReference` trait to make `chunking_type()` and `binding_usage()` methods return direct values instead of `Vc<T>` wrapped values, removing the need for async task functions.

Also removes the `get_referenced_asset` task from `EsmAssetReference`, inlining its logic into the callers.

### Why?

This change simplifies the API by eliminating unnecessary async overhead for methods that typically return simple, computed values. The previous implementation required `#[turbo_tasks::function]` annotations and `Vc<T>` wrappers even when the methods didn't need to perform async operations or benefit from caching.

### Impact

| Metric | Base | Change | Delta |
|--------|------|--------|-------|
| Hits | 35,678,143 | 35,845,124 | **+166,981** |
| Misses | 9,418,378 | 7,910,986 | **-1,507,392** |
| Total | 45,096,521 | 43,756,110 | **-1,340,411** |
| Task types | 1,306 | 1,277 | **-29** |

29 task types were removed, eliminating **2.6M total task invocations** (1.1M hits + 1.5M misses):

- **`chunking_type`** — 21 task types removed across all `ModuleReference` implementors (~952k invocations)
- **`binding_usage`** — 6 task types removed (~527k invocations)
- **`BindingUsage::all`** — helper task removed (~36k invocations)
- **`EsmAssetReference::get_referenced_asset`** — removed and inlined (~1.08M invocations: 628k hits + 451k misses)

The removed `get_referenced_asset` hits reappear as +628k hits on `EsmAssetReference::resolve_reference` and `ReferencedAsset::from_resolve_result` (with zero increase in misses), confirming the work is now served from cache through the existing callers.

No tasks had increased misses — the removal is clean with no cache invalidation spillover.

I also ran some builds to measure latency

```
# This branch
$ hyperfine -p 'rm -rf .next' -w 2 -r 10  'pnpm next build --turbopack --experimental-build-mode=compile'
Benchmark 1: pnpm next build --turbopack --experimental-build-mode=compile
  Time (mean ± σ):     52.752 s ±  0.658 s    [User: 376.575 s, System: 106.375 s]
  Range (min … max):   51.913 s … 54.161 s    10 runs

# on canary
$ hyperfine -p 'rm -rf .next' -w 2 -r 10  'pnpm next build --turbopack --experimental-build-mode=compile'
Benchmark 1: pnpm next build --turbopack --experimental-build-mode=compile
  Time (mean ± σ):     54.675 s ±  1.394 s    [User: 389.273 s, System: 114.642 s]
  Range (min … max):   53.434 s … 58.189 s    10 runs
```

so a solid win of almost 2 seconds

MaxRSS also went from 16,474,324,992 bytes to 16,359,309,312 bytes (from one measurement) so a savings of ~100M of max heap size.


### How?

- Changed `chunking_type()` method signature from `Vc<ChunkingTypeOption>` to `Option<ChunkingType>`
- Changed `binding_usage()` method signature from `Vc<BindingUsage>` to `BindingUsage`
- Removed `ChunkingTypeOption` type alias as it's no longer needed
- Updated all implementations across the codebase to return direct values instead of wrapped ones
- Removed `#[turbo_tasks::function]` annotations from these methods
- Updated call sites to use `into_trait_ref().await?` pattern when accessing these methods from `Vc<dyn ModuleReference>`
- Removed `EsmAssetReference::get_referenced_asset`, inlining its logic into callers
- Added validation for `turbopack-chunking-type` annotation values in import analysis
- Fixed cache effectiveness analysis script
2026-03-13 11:54:56 -07:00
JJ Kasper 07189c5224 Expose edge runtime fields in build-complete (#91239)
### What?

- Add canonical edge entrypoint metadata to edge function definitions in
the middleware manifest.
- Surface that canonical entrypoint through `build-complete` as
`edgeRuntime` metadata in adapter outputs.
- Update middleware and adapter tests to assert the new metadata instead
of relying on bundler-specific emitted file names.

### Why?

Adapter consumers need a stable way to load and invoke edge outputs
without depending on `next-server`'s sandbox handling or guessing which
emitted file is the executable entrypoint. The existing checks were also
brittle across webpack and Turbopack because the emitted file lists can
differ.

### How?

- Add an optional `entrypoint` field to `EdgeFunctionDefinition` and
populate it in both the webpack middleware manifest generator and the
Turbopack manifest emitters for app, pages, and middleware edge
functions.
- In `packages/next/src/build/adapter/build-complete.ts`, use that
canonical entrypoint for `filePath` and expose `edgeRuntime.modulePath`,
`edgeRuntime.entryKey`, and `edgeRuntime.handlerExport` for edge
outputs.
- Adjust the middleware manifest tests to validate cross-bundler
invariants and add adapter-config coverage for the new `edgeRuntime`
fields.
2026-03-13 18:22:47 +00:00
Benjamin Woodruff d7de6f105c Turbopack: Remove the IntoTraitRef trait, make it an inherent method on Vc (#91223)
This is basically one-shot from Opus 4.6, with a minor tweak to the doc comment by hand.

Discussed this with @lukesandberg, and we realized that there's only one implementation of this trait, so it doesn't make a ton of sense as a trait.
2026-03-13 10:43:28 -07:00
JJ Kasper a6b36200ff Wire cache handlers in edge paths and add e2e regression coverage (#91236)
### What?

This PR wires custom `cacheHandler` / `cacheHandlers` through the
remaining edge entrypoints so edge app pages, edge app routes, edge
pages SSR, middleware, and edge API routes can all see the configured
handlers.

It also adds an e2e regression suite covering:
- pages-router ISR revalidation with a custom incremental cache handler
- app-router cache handlers with cache components enabled
- edge app page + edge app route wiring when cache components are
disabled

### Why?

We already support custom cache handlers in non-edge paths, but several
edge code paths were not forwarding that configuration into the
generated entrypoints/runtime wrappers. That meant custom cache handlers
could be skipped in edge rendering and edge route execution, and we did
not have regression coverage around those cases.

### How?

- pass `cacheHandler` / `cacheHandlers` through the JS build entry
plumbing for edge server entries
- inject cache handler imports/registration into the webpack edge
templates and loaders
- mirror the same wiring in the Turbopack/Rust entry generation for edge
app pages, app routes, pages SSR, and middleware
- update the edge route wrapper to initialize and register cache
handlers before invoking the route module
- extend `next-taskless` template expansion with raw injection support
so the generated edge templates can add imports and registration code
- add `test/e2e/cache-handlers-upstream-wiring` fixtures to cover
pages/app, edge/non-edge, and revalidation behavior
2026-03-13 17:25:59 +00:00