Commit Graph

35737 Commits

Author SHA1 Message Date
Niklas Mischkulnig 529d290b8f [test] Unpin Vercel CLI version (#97974)
Reverts #97960
vercel@59.7.0 should fix this again
2026-08-27 10:07:14 +02:00
Niklas Mischkulnig ef6e23fa70 Turbopack: don't replace single-arg calls with argument in analyzer (#95277)
This was a hack added a long time ago. And it can lead to correctness
issues
2026-08-27 10:06:43 +02:00
Niklas Mischkulnig 4160afe2c6 Turbopack: allow compiling turbopack-node without a pool backend (#97943)
It failed to compile thus far if you didn't set any features.

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 09:58:44 +02:00
Niklas Mischkulnig 80f846c74e Support immutable static assets with output: 'export' (#97711)
There was no real reason why this wasn't done thus far.
`output: 'export'` already uses the adapter anyway.

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 09:55:24 +02:00
Niklas Mischkulnig d75bf1b85a Fix build error when aliasing typescript to @typescript/typescript6 (#97942)
This was previously broken, because the binary isn't called `tsc` but
`tsc6`:
```json
"dependencies": {
	"typescript": "npm:@typescript/typescript6@6.0.1"
}
```

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-27 09:49:52 +02:00
Niklas Mischkulnig d6e5741682 ci: remove pull_request_stats workflow (#97792)
This was quite useless (very flaky) and furthermore also slow
2026-08-27 09:43:29 +02:00
next-js-bot[bot] d4b7cc1fb4 v16.4.0-canary.9 v16.4.0-canary.9 2026-08-27 00:23:12 +00:00
jgruica 97c6aedf18 fix: don't drop client references when the concatenated module id is 0 (#97936)
### What?
`getModuleId()` returns `string | number | null`, and `0` is a valid id.
The`ConcatenatedModule` fallback guards it with a truthiness check, so
when the concatenated module gets id `0` the client entry is left out of
the client reference manifest.

### Why?
The
[code](https://github.com/vercel/next.js/blob/d09816fdaf2fef7a23751de75a9dde6bb7c2403b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts#L516)
right above it already checks `modId !== null`. The two checks sit a few
lines apart and disagree about whether `0` is a valid id, which looks
like an oversight. When it happens the build fails with `Could not find
the module ... in the React Client Manifest`, which doesn't point at the
cause.

Repro: https://github.com/jgruica/next-concat-modid-zero (needs
`--webpack` plus `concatenateModules: true`, so it's rare in practice)

### How?
One line in `flight-manifest-plugin.ts`: `if (concatenatedModId)`
becomes `if (concatenatedModId !== null)`. `null` is still skipped, so
nothing elsechanges.

Co-authored-by: Marcos Hernanz <96699542+marcoshernanz@users.noreply.github.com>
2026-08-26 17:20:34 -07:00
Jiwon Choi a65ccea59c Replace resumed render bailout error with ReactDOM.browser behind a flag (#96844)
Stacked on #96843.

This PR replaces the bailout error used to abort resumed renders with
`ReactDOM.browser()` behind the experimental flag.

### How?

Pass the flag to the App Router runtime, use `ReactDOM.browser()` as the
abort reason when enabled, and preserve existing behavior otherwise.

<!-- NEXT_JS_LLM -->
2026-08-27 02:07:27 +02:00
Jiwon Choi c11c48e42d Replace useSearchParams bailout error with ReactDOM.browser behind a flag (#96843)
Stacked on #96826.

This PR replaces the bailout error used by `useSearchParams` with
`ReactDOM.browser()` behind the experimental flag.

### How?

Move the bailout behind a browser-safe module boundary, use
`ReactDOM.browser()` when enabled, and preserve existing behavior
otherwise.

<!-- NEXT_JS_LLM -->
2026-08-27 02:07:26 +02:00
Jiwon Choi 23985382a7 Replace CSRBailout error with ReactDOM.browser behind a flag (#96826)
CSRBailout error is the use case for `ReactDOM.browser()`. Replace
behind an experimental flag.
This PR replaces the bailout error used in next/dynamic ssr: false.
2026-08-27 02:07:26 +02:00
Muhammad Zeeshan fad317505d fix(next/image): reject non-2xx internal image responses (#97957)
- Fixes https://github.com/vercel/next.js/issues/82357
- Closes https://github.com/vercel/next.js/pull/96985

### What?

`fetchInternalImage()` only rejects the internal response when
`statusCode` is falsy, and `MockedResponse` defaults it to `200`, so
nothing really gets rejected there. A `307` coming from a `redirects()`
entry in `next.config`, or a `404` for a path that isn't there, both
count as a successful image fetch.

That body then reaches `detectContentType()`, which returns `null`, and
all you get is:

```
The requested resource isn't a valid image for /path/to/image.png received null
```

Nothing in that line points back at the response that actually came in,
so a redirect looks exactly like a corrupt image.

### Why?

`fetchExternalImage()` right above already covers this with `if
(!res.ok)` and logs `res.status`. The internal path is the only fetch in
the file without that check, so the same failure gets reported at the
fetch for remote images, and three steps later as "not a valid image"
for local ones.

### How?

The guard now rejects anything outside the 2xx range, which is what
`res.ok` means, and the log line carries the status next to the href.
The thrown `ImageError` message stays generic, same as the external
path, so nothing new shows up in the HTTP response body.

Because that throw passes `mocked.res.statusCode` into `ImageError`, a
non-2xx internal response now reports its own status instead of always
landing on `400`. `ImageError` still maps anything under 400 to `500`,
so a redirect answers `500` and a `404` stays `404`. That is what
`fetchExternalImage()` has always done, and it lines the two up: a
missing local image now returns `404` just like a missing remote one.

Three assertions in `test/e2e/image-optimizer/util.ts` shift because of
that:

- `should not forward cookie header`: `/api/conditional-cookie` answers
`401` when the cookie is missing, so `400` becomes `401`
- `should error if the image file does not exist`: `/does_not_exist.jpg`
returns `404` now, with the internal response message
- `should error if the resource isn't a valid image`: this one requested
`/test.txt`, but the fixture in `public/` is `text.txt`, so it was
quietly exercising a missing file instead of a non-image file. Pointed
at the file that exists, it keeps its original `400` and finally tests
what its name says

Two unit tests cover the new guard, one `307` and one `404`. Every other
test in that file sets `statusCode = 200`, so they are untouched.

#96985 is open against the same report with a different angle: it
threads the status through `ImageUpstream` so the existing "isn't a
valid image" log can print it. This one stops the request at the fetch,
so the redirect never reaches the optimizer at all. Whichever one fits
better, feel free to close mine.

### Verification

- `jest test/unit/image-optimizer/`: 139/139, and the two new tests fail
against a build without the guard
- `pnpm test-dev
test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97
- `pnpm test-start
test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97
- `prettier --check` clean on the three changed files

<!-- NEXT_JS_LLM -->

Co-authored-by: Steven <steven@ceriously.com>
2026-08-26 17:04:32 -07:00
Sebastian "Sebbie" Silbermann 49f791cc98 [test] Pin Vercel CLI to Node 20.9 compatible version (#97960)
- latest resolved to vercel@59.6.2.
- It loads `@vercel/node@8.1.0` -> `@vercel/static-config@3.4.2`.
- `@vercel/static-config` is CommonJS and calls `require("oxc-parser")`.
- `oxc-parser@0.121.0` is ESM-only and requires Node `^20.19.0 ||
>=22.12.0`.
- CI is running Node 20.9.0, which cannot synchronously
2026-08-26 22:41:16 +00:00
Sebastian "Sebbie" Silbermann 1a7ccf481d Re-enable AVIF image optimization (#97931)
Reverts #97875 and bumps `sharp` from `^0.35.3` to `^0.35.4`,
re-enabling AVIF image optimization.


`minimumReleaseAge` interplay (verified with pnpm 10.33.0 and 11.22.0 in
a standalone repo with a `file:` dependency declaring sharp as an
optional dependency):

- sharp 0.35.4 was published on 2026-08-26 and is younger than this
repo's 48h `minimumReleaseAge`.
- pnpm aborts the whole install with
`ERR_PNPM_NO_MATURE_MATCHING_VERSION` when no in-range release satisfies
the age gate. This applies to `optionalDependencies` (no warn-and-skip)
and through `file:` dependencies alike.
- The gate also covers the `@img/sharp-*` binary packages sharp 0.35.4
depends on, so excluding only `sharp@0.35.4` is not enough.
- `pnpm-workspace.yaml` therefore exempts `sharp@0.35.4` and
`@img/sharp-*` (pnpm does not allow version qualifiers on name patterns
in this setting). Both entries can be dropped once 0.35.4 is older than
48 hours.
- Downstream users who configure their own `minimumReleaseAge` will hit
the same hard install failure when installing a release with this bump
until 0.35.4 ages out, unless they add the same excludes. Keeping
`^0.35.3` is not a safer alternative: age-gated installs would then
silently resolve the vulnerable 0.35.3.

Part of https://linear.app/vercel/issue/VOC-34654/

---------

Co-authored-by: Steven <steven@ceriously.com>
2026-08-26 23:28:06 +02:00
Lazizbek Ergashev 0c7f81e6a3 Don't report a client-aborted RSC stream as a render error (#96715)
When a client disconnects mid-stream, `pipeNodeReadableToNodeResponse`
destroys the readable React is piping into. React sees its destination
close before the render ended and aborts with a generic `Error("The
destination stream closed early.")`, which `onError` has no way to
distinguish from a real render error, so it ends up in `onRequestError`
as a server render error.

The Web streams path doesn't have this problem: cancelling the readable
passes the `ResponseAborted` reason straight through to React. This does
the same for the Node path by aborting the pipeable with that reason as
soon as the passthrough is destroyed early, before React's own close
handler runs. Genuine render errors still go through untouched.

Fixes #96704

---------

Co-authored-by: Jude Gao <jude.gao@vercel.com>
2026-08-26 16:26:01 -04:00
Janka Uryga d352f1740a [PPF] Only track runtime accesses when the promise is used (#97165)
Improves how we track accesses of runtime data in static prerenders
(which affects whether a page will use static or runtime shells).
Now, Instead of tracking it when the hanging promise is created, we'll
track it when `then/catch/finally` is called on the promise (which we
use as a signal for being used).

The promise tracking required some novel work, because:

- Using a proxy seems to cause `Promise.then` to show up in call stacks
(we've had issues with proxies throwing off React's IO tracking before,
i believe this is a similar problem)
- patching `then/catch/finally` on the promise directly doesn't work,
because `then` doesn't get called for native `await`

The only way of doing this that i've found is a custom `Promise`
subclass that tracks the access in its `then` implementation. This is
what `TrackedPromise` does.

To improve the debuggability, i've added some debug logs enabled by
`NEXT_PRIVATE_DEBUG_RUNTIME_DATA=1` to detect when a page is deopted to
runtime and what expression caused it.

---

This fixes a false deopt caused by `createServerPathnameForMetadata` -
when called on a route with fallback params, it would always track a
fallback params access even if no icons/og images that actually use it
exist. This meant that we'd never statically optimize any routes that
had fallback params. After this PR, we only track a runtime data access
if a metadata route that uses that params actually exists, in which case
the `pathname` promise gets awaited.
(as a future optimization, we should do the equivalent of vary params
tracking on metadata routes to know if the params are used at all. also,
the logic in `pathname.ts` is generally in need of cleanup)
2026-08-26 18:05:44 +02:00
Tobias Koppers 0304c4ff8d Fix Turbopack re-export cycle deadlock (#97933)
### What?

Prevents Turbopack from deadlocking while resolving re-exports through
an import cycle by making ECMAScript side-effect classification
independent of full module analysis.

The supplied async regression is added to the normal execution suite,
along with a synchronous variant demonstrating that the facade/locals
re-export cycle itself is sufficient to trigger the issue.

### Why?

Cross-module constant analysis resolves eligible imported bindings while
the importing module's analysis is still running. Re-export following
asks synthesized locals modules for side effects, which delegate to
their original modules. The previous fallback obtained side effects from
the full `AnalyzeEcmascriptModuleResult`, so an import cycle could
re-enter an already-running analysis task and close a Turbo Tasks await
cycle.

The actual static side-effect classifier only needs the parsed SWC
program, comments, unresolved mark, directives, and module options.
Reference analysis is not required.

This also unblocks the export-mangling work in #97770.

### How?

Side-effect classification now runs in a dedicated parse-only Turbo
Task. Package/config declarations retain precedence; when none exists,
the task uses `failsafe_parse()` and the existing AST classifier without
resolving references or awaiting `analyze()`.

`AnalyzeEcmascriptModuleResult.side_effects` and its builder state are
removed because the module-side-effects fallback was its only consumer.
This also eliminates unused duplicate classification during module-part
analysis.

`follow_reexports` and module-fragments side-effect handling remain
unchanged from canary.

### Verification

- `cargo test -p turbopack-tests --test execution` — 265 passed, 0
failed
- `cargo test -p turbopack-ecmascript` — 525 passed, 0 failed
- Focused re-export-cycle and side-effect-retention tests in both
optimization modes
- `cargo fmt -- --check`

<!-- NEXT_JS_LLM -->


<!-- fleet 4bbd5119-2442-4053-a856-f9d596924113 -->

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-26 18:03:27 +02:00
Andrew Clark 94327e8ade Port React's @gate test directive to the e2e harness (#96228)
The test suite has accumulated a bunch of patterns for disabling tests
that are known to fail under some configuration: `it.skip`, `if
(isNextDev) { test('skipped in dev mode', () => {}); return }`, whole
describes toggled off by checking `process.env.__NEXT_CACHE_COMPONENTS`.
These all have the same flaw: nothing tells you when the thing you
skipped starts working. The test stays disabled forever, and the
workaround it was guarding rots along with it.

React solves this with the `@gate` pragma, and this PR ports it to the
Next.js e2e harness:

```ts
// Blocked on the optimization that marks a route as fully static when
// no dynamic params are referenced in Server Components.
// @gate !cacheComponents
it('navigates to a page with a lazily-generated static param', async () => {
  // body unchanged
})
```

The test still runs. If the condition is false and the test fails, the
failure is absorbed and the suite stays green. If it _passes_, the suite
fails: the gate is stale, delete it. So instead of a skip that hides a
fixed bug indefinitely, you get a CI failure the day the fix lands.

When the condition is static, the inversion is Jest's own `test.failing`
under the hood. A lazy condition isn't known until the fixture's
resolved config is read inside the body, so those tests invert at
runtime instead.

`// @force-gate <condition>` skips for real — for tests that can't even
be attempted (prefetching is disabled in dev, deploy has no local build
output, the fixture won't build under the condition), and for tests of a
new API, where the disabled state can only throw and running it proves
nothing:

```ts
// Prefetching is disabled in dev, so this suite has nothing to test.
// @force-gate prefetching
describe('segment cache prefetch scheduling', () => {
  // ...
})
```

There's no staleness check in that case, so this is a judgment call:
prefer `@gate` when the off state fails for a meaningful reason — the
flag changes behavior that already exists — and `@force-gate` when the
body can only throw because the API doesn't exist. A static condition
(mode, bundler) resolves at collection time into a normal Jest skip. A
lazy condition resolves at runtime, and when a lazy force-gate on a
describe is false, we skip the fixture build entirely — that's what
makes it usable for suites whose fixtures are build-incompatible with
the condition. (One caveat: Jest has no way to skip a test that's
already running, so these report as passing with a warning in the log,
not as skipped.)

Conditions live in a hand-written registry. I considered deriving the
lazy ones from the config schema automatically, but a gate is a claim
about which dimension of the test matrix explains a failure, and I'd
rather each of those claims be spelled out with a description.
Referencing an undeclared name fails the suite at collection time, so a
typo can't silently disable a gate.

The important design decision for lazy conditions is that they read the
fixture's _resolved_ config, never `process.env`. The env var isn't the
truth: `__NEXT_CACHE_COMPONENTS=true` only applies when the fixture
doesn't set `cacheComponents` itself, and config resolution implies
flags the fixture never mentions (`cacheComponents: true` alone turns on
`experimental.ppr`). Resolution happens in a child process, because
in-process `loadConfig` would leak the fixture's `.env` files into the
Jest worker. Suites with no lazy gate never pay for any of this.

The condition expression is parsed using a small grammar (also ported
from the React repo). An expression that doesn't parse fails the suite:

```ts
// @gate mode === 'start' && !cacheComponents
// @gate !(turbopack || rspack)
```

There's also a runtime version, mirroring React's `gate(flags =>
flags.enableFoo)`, for tests that run under both states but assert
differently (and for `it.each`, where the pragma can't attach):

```ts
import { gate } from 'next-test-utils'

it('renders the fallback', async () => {
  if (await gate((conditions) => conditions.cacheComponents)) {
    // PPR: the fallback is part of the static shell
  } else {
    // fully dynamic: the fallback streams in
  }
})
```

It also accepts the pragma expression language as a string: `await
gate('cacheComponents && !dev')`.

Docs are in `test/lib/gate/README.md`; `test/unit/gate/` covers the
transform, the expression language, and the runtime.
2026-08-26 10:22:11 -04: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
Niklas Mischkulnig d09816fdaf Turbopack: expose list of non-inlined env vars (#95310)
Add a new method to `EcmascriptAnalyzable` to return the list of
non-inlined env vars

In this PR, this isn't used anywhere outside of the tests yet

While `cargo bench -p turbopack-ecmascript` does regress:
```
Benchmarking references/packages-bundle.js/full: Warming up for 1.0000 s
references/packages-bundle.js/full
                        time:   [152.99 ms 153.34 ms 153.70 ms]
                        change: [+11.939% +12.678% +13.307%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 4 outliers among 100 measurements (4.00%)
  4 (4.00%) high mild
Benchmarking references/packages-bundle.js/tracing: Warming up for 1.0000 s
references/packages-bundle.js/tracing
                        time:   [153.07 ms 153.92 ms 155.15 ms]
                        change: [+34.997% +36.345% +37.554%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 4 outliers among 100 measurements (4.00%)
  2 (2.00%) high mild
  2 (2.00%) high severe
```

There is no measurable overall perf impact:
```
commit 7d29e358c8 (HEAD, tag: v16.4.0-canary.5)
    v16.4.0-canary.5

pnpm next build --experimental-build-mode=compil  400.89s user 51.96s system 747% cpu 1:00.61 total
pnpm next build --experimental-build-mode=compil  394.99s user 54.10s system 757% cpu 59.289 total
pnpm next build --experimental-build-mode=compil  399.87s user 58.71s system 772% cpu 59.398 total
```
vs
```
commit 9335597135118b8332b8e0d5a3bcedb596a3ef07 (HEAD -> mischnic/env-var-references)
    remove runtime_all

pnpm next build --experimental-build-mode=compil  399.83s user 50.09s system 719% cpu 1:02.57 total
pnpm next build --experimental-build-mode=compil  403.57s user 53.79s system 743% cpu 1:01.52 total
pnpm next build --experimental-build-mode=compil  402.23s user 55.05s system 758% cpu 1:00.29 total
pnpm next build --experimental-build-mode=compil  400.17s user 55.15s system 761% cpu 59.817 total
```
2026-08-26 14:46:53 +02:00
Tobias Koppers 130ea3227a feat(turbo-tasks-fetch): stub HTTP on wasm targets (#97585)
### What?

Reports HTTP as unsupported on wasm, in the shape of the `reqwest` API
`turbo-tasks-fetch` consumes,
so the crate compiles for wasm targets.

### Why?

`reqwest` cannot serve wasm here. Its only wasm backend targets
`wasm32-unknown-unknown` and is built
on the browser `fetch` API via `wasm-bindgen`. Under
`wasm32-wasip1-threads` that backend is still
selected (it keys off `target_arch = "wasm32"`), and it is both
API-incompatible (no
`ClientBuilder::connect_timeout`, no `ClientBuilder::timeout`, no
`Error::is_connect`) and — fatally —
**`!Send`**, which `turbo-tasks` requires of every task future. WASI
preview1 has no sockets to build a
native client on either.

### How?

This is deliberately **not** a client and does not pretend to be one:
building a client fails
immediately with a clear error, so no request is ever attempted, nothing
is retried, and no response is
ever produced. The remaining types exist only so the shared fetch code
type-checks, and the paths that
could never be reached say so rather than returning plausible dummy
values.

The error names the consequence and the alternatives, rather than
surfacing as a generic failure:

> HTTP requests are not supported in wasm builds of Next.js: this
platform has no HTTP client.
> Features that fetch at build time, such as `next/font/google`, cannot
be used here — self-host the
> assets, or use a platform with native Next.js binaries.

### Follow-up

Real support needs the **host** to provide HTTP: for wasm builds of the
Next.js bindings that means a
napi callback into the JS `fetch`, kept behind the existing
`FetchClientConfig` interface so callers are
unaffected. That is a separate PR by design — a `ThreadsafeFunction`
call from a wasi thread cannot be
validated until the wasm bindings can be instantiated, which is blocked
on the unreleased emnapi v2.


<!-- 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-26 14:36:37 +02:00
Tobias Koppers e94513bfa3 fix(turbo-rcstr): make TaggedValue usable on wasm (#97577)
### What?

Makes `turbo-rcstr`'s tagged value work on wasm — and on any target
whose pointer is narrower than the
value — **without changing the representation or the inline capacity on
any target**. `RcStr` stays 8
bytes wide everywhere, `MAX_INLINE_LEN` stays 7 everywhere, and `rcstr!`
stays a `const`.

### Why?

`TaggedValue` stored a `NonNull<()>` on 64-bit but a bare `NonZeroU64`
when
`target_pointer_width = "32"`. `rcstr!` expands to a `const`, so both
constructors must be usable
during const evaluation — and with integer storage `new_ptr` needs a
pointer→integer cast, which
const-eval forbids. Every `rcstr!` taking the static path failed to
compile:

```
error[E0080]: unable to turn pointer into integer
```

Concretely, `cargo check -p turbopack-core --target
wasm32-wasip1-threads` reported **4** of these
before this change and **0** after.

### How?

The fix rests on an asymmetry in const evaluation:

| direction | const-eval | why |
|---|---|---|
| pointer → integer | **forbidden** | a pointer is an abstract
(allocation, offset) pair; its numeric address doesn't exist until the
linker assigns one |
| integer → pointer | allowed | the result carries no provenance, which
is fine as long as it is never dereferenced |

64-bit already only needs the legal directions: `new_ptr` is
pointer→pointer, `new_tag` is
integer→pointer. So rather than narrow the value on other targets, they
now store a struct that keeps
a **real pointer field**:

```rust
#[repr(C, align(8))]
struct RawLittle { ptr: NonNull<()>, pad: [u8; PAD] }
```

`new_ptr` stores the pointer as a pointer; `new_tag` transmutes an
integer into the struct. Reading the
value back as an integer (`get_ptr`, `get_value`, `tag_byte`) only
happens at **run time**, where
pointer→integer is perfectly legal.

Three details are load-bearing, and each has a static assertion:

- **`align(8)`** — a pointer field alone would align the struct to 4 on
wasm32, and reading the value
as a `u64` then traps with `RuntimeError: operation does not support
unaligned accesses`.
- **No padding** — the byte payload is sized to the pointer width,
because padding bytes are
uninitialised and would make the whole-value transmute in `new_tag`
invalid.
- **Endian-dependent field order** — the pointer's least-significant
byte must land where the value's
least-significant byte lives (offset 0 little-endian, last byte
big-endian), since that is where the
tag is read from. Both layouts are declared unconditionally so the
big-endian assertion still
compiles (and was verified to fail when deliberately broken) on a
little-endian host.

A union satisfies both constructors too, but unions carry no niche in
rustc, so `Option<RcStr>` would
grow to 16 bytes. The struct keeps it at 8 — also asserted.

### Testing

- `turbo-rcstr`: **11 passed** on host, **10 passed / 1 ignored** on
`wasm32-wasip1-threads`
(the ignored one needs the static-`RcStr` registry, which depends on the
link-section loader hook).
- Round-trips every `rcstr!` length from 0 to 9 on both targets,
covering both sides of the inline
  boundary and the static path.
- Each of the new static assertions was verified to **fail** when
deliberately broken, including the
  big-endian layout one.
- `MAX_INLINE_LEN == 7` asserted on host and wasm.
- The five `const … = rcstr!(…)` sites in the tree — including two `pub
const`s in `turbopack-core` —
  are untouched and still compile as `const`.
- `atom_size_128` still builds; its pre-existing const limitation is
unchanged and out of scope.
- `cargo fmt --check`, and clippy `-D warnings` for the host and for
`wasm32-wasip1-threads`.

Big-endian and 16-bit are compile-checked and assertion-protected, but
not runtime-exercised — no such
target is available here.

<!-- 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>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
2026-08-26 14:36:37 +02:00
Joseph efd592e9d0 docs: explain origin matching for allowedOrigins and allowedDevOrigins (#97805)
The `allowedDevOrigins`, and `allowedOrigins` options, behave slightly
different to each other, the former drops the PORT, and it is not clear
where are these picked up from. This PR improves guidance on how to use
these two.
2026-08-26 10:29:42 +00:00
Niklas Mischkulnig 39a16acee2 Turbopack: improve Pat::Assign modelling in analyzer (#97867)
The value of the argument is actually `Argument | Default value`.
Previously, the `take_pat_value` would take it and the recursive
`visit_pat` would not see it anymore.
2026-08-26 12:15:50 +02:00
Niklas Mischkulnig 3dd04f117c Upgrade rustc to nightly-2026-08-20 (#97665)
All platform build run:
https://github.com/vercel/next.js/actions/runs/32464645938
2026-08-26 10:14:46 +00:00
next-js-bot[bot] 9ce5575a71 Upgrade React from bd6ea412-20260824 to f789f203-20260825 (#97887) 2026-08-26 06:41:54 +00:00
Tobias Koppers 2059206d96 [test] Fix flaky build CLI output capture (#97900)
### What?

Stabilize `NextStartInstance.build()` CLI output capture so failed
production builds return all trailing stdout/stderr diagnostics.

The existing `sync-io-blocks-root` behavioral assertions remain
unchanged, including the route-specific `Date.now()` diagnostic,
timeout/retry guard, and exit-code check.

### Why?

The `--debug-prerender` cases intermittently lost the detailed
`Date.now()` diagnostic from `result.cliOutput` in CI even though builds
exited normally. Across the reported CI run and two retries, the missing
route set varied from 4 to 2 to 3 routes, consistent with an
output-drain race rather than a rendering or resource failure.

On latest canary with Node 20, `NEXT_TEST_MODE=start`, and
`IS_WEBPACK_TEST=1`, 5 baseline campaigns completed 24 builds (20
debug-prerender) without reproducing the intermittent failure locally. A
controlled Node 20 reproduction established the lifecycle race directly:
the child `exit` event observed no trailing output, while `close`
observed the trailing diagnostic.

### How?

Wait for the child process `close` event before snapshotting
`cliOutput`. Unlike `exit`, `close` is emitted after the piped stdout
and stderr streams close, so diagnostics emitted near process shutdown
are fully drained before the build result is returned.

This is intentionally limited to the affected start-mode build harness
and is independent of the mmap persistence PR stack (#97873, #97888,
#97889).

### Verification

- Baseline on Node 20.20.2 / start mode / Webpack: 5 campaigns, 24
builds total, 20 debug-prerender; 0 failures and no affected local
routes
- Deterministic child-process lifecycle reproduction: `exit` captured
empty output; `close` captured the trailing diagnostic
- Post-fix focused test: 5/5 runs passed, covering 20/20 debug-prerender
cases
- Post-fix complete test: 3/3 runs passed, covering 24/24 cases (12
debug-prerender and 12 normal production)
- `pnpm build-all`
- Prettier and ESLint for `test/lib/next-modes/next-start.ts`
- `pnpm types`

<!-- NEXT_JS_LLM -->


<!-- fleet a8a423ef-2679-4cb0-ba51-92592630165e -->

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-26 08:15:50 +02:00
Luke Sandberg e8ed144507 turbo-tasks-backend: parent_count reference counting (#95976)
Adds the reference counting that lets the backend know when a task has
become unreachable.

### What this adds

Two inline counts on `TaskStorage`, maintained incrementally so a task's
reachability is known the moment its last parent drops — with no DB
scan:

- **`parent_count`** (meta, persisted) — how many persistent parents
list this task in their `children` set. Reaching 0 means the task is
unreachable from persistent roots.
- **`transient_ref_count`** (transient) — session-only references that
aren't persistent parent edges: a transient parent's child edge, or a
detached handle held outside the tracked graph. Never persisted;
re-established on restart.

Maintenance happens at the two `children`-edge mutation sites:

- `connect_children` applies the `+1` directly under the child guard it
already takes. It has to land before the first `operation_suspend_point`
so the new child edge and the `+1` are atomically observable.
- `CleanupOldEdges` queues the `-1` as an aggregation-update job, which
takes the child guards itself rather than nesting a second guard under
the parent's.

`AdjustParentCount` is durably encoded so an in-flight delta replays
after a crash and the persisted count stays consistent.
`AdjustTransientRefCount` is `bincode(skip)` and collapses to `Noop`,
like the other transient active-count jobs.

### Two things worth a closer look

**`output_dependent`'s inline capacity drops from 6 to 4.**
`test_schema_size` asserts `TaskStorage` is exactly 128 bytes, and the
two new `u32`s add 8. `AutoSet<TaskId, 6>` is 32 B and `AutoSet<TaskId,
4>` is 24 B, freeing exactly the 8 needed (capacity 5 is still 32 B, so
4 is the first that fits). This is a real behavior change, not just a
layout tweak: a task with 5 or 6 output dependents now spills to the
heap where it previously stayed inline.

**`task_execution_completed_unfinished_children_dirty` is gone.** Since
`connect_children` now takes a guard on every new child anyway, the
"mark children with no output dirty" pass folds into that same loop —
one guard acquisition instead of two. Behavior-preserving relocation,
but it does mean the dirty-marking now reads as part of the connect
path.

### Testing

`tests/parent_count.rs` covers the count reaching the right value
through connect/disconnect, the parallel wide-fanout `connect_children`
path, revalidation not double-counting, survival across a snapshot +
eviction, and survival across a full DB reopen (the count is persisted
meta, and an in-flight `AdjustParentCount` replays via the durable
queue).

Each of the four commits builds and tests green on its own.
2026-08-25 19:26:08 -07:00
next-js-bot[bot] bc80691cd0 v16.4.0-canary.8 v16.4.0-canary.8 2026-08-25 23:23:07 +00:00
Marcos Hernanz 19ebdc4cba Fix Turbopack resolution through chained symlinks (#97825)
## Summary

- classify symlinks by the final target of the full chain with
`LinkTarget::resolved_type`
- preserve non-directory behavior for dangling, invalid, and cyclic
chains and retain the existing ancestor-loop guard
- fold two-hop directory-symlink coverage into the existing production
symbolic-links suite

Fixes #97786.

## Verification

- `CARGO_INCREMENTAL=0 pnpm build-all`
- `cargo test -p turbo-tasks-fs
test_link_target_resolved_type_through_chain -- --nocapture`
- `cargo test -p turbo-tasks-fs read_glob_symlinks -- --nocapture`
- `cargo test -p turbopack-core test_read_matches -- --nocapture`
- `cargo clippy -p turbo-tasks-fs -p turbopack-core --all-targets -- -D
warnings`
- `cargo fmt --all -- --check`
- `pnpm test-start-turbo test/production/app-dir/symbolic-links -t
"chained directory symlinks"`
- manually verified `/directory-symlink-chain` under `next dev
--turbopack`

---------

Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
2026-08-25 14:58:30 -07:00
Jude Gao f7ff5fd588 Add evals for whether agents actually use Next.js (#97816)
Adds evals that check whether an agent reaches for Next.js when asked to
build something, and whether what it ships is really Next.js rather than
something that only resembles it.
2026-08-25 20:38:51 +00:00
Jude Gao f5f4ab6ae9 evals: judge behavior instead of matching source text (#97826)
Makes a few of the evals judge what the code actually does rather than
how it happens to be written, so correct solutions stop failing on
style. One of them was already being vetoed by leftover assertions the
judge was meant to replace.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 20:11:46 +00:00
Jude Gao fcf66c3773 Bump @vercel/agent-eval to 2.2.1 (#97885)
Harness bump so the eval grader rewrite in #97826 can run against the
fixed judge.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:10:50 +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
Sebastian "Sebbie" Silbermann 7dcf6c0a68 [ci] Use short-lived access tokens for preview-build uploads instead of static token (#97256)
Replaces the static `PREVIEW_BUILDS_BLOB_READ_WRITE_TOKEN` secret with a
short-lived access token to upload to Vercel Blob in exchange for a
GitHub OIDC (endpoint side:
https://github.com/vercel/vercel-packages/pull/98):

- the job mints GitHub Actions OIDC tokens for the
`https://vercel-packages.vercel.app` audience itself (the workflow gains
`id-token: write`) and re-mints shortly before expiry since a token
lives about five minutes while a package batch can take longer
- `upload-preview-tarballs.js` exchanges the token at vercel-packages
for scoped client-upload tokens via POST and uploads the tarball bytes
directly to Blob storage with `@vercel/blob/client`'s `put`, so the blob
read-write token never leaves vercel-packages
- the `preview-builds` environment is dropped from the job: it only
existed to selectively expose the static token and restrict uploads to
canary, and the canary binding is now enforced by the `job_workflow_ref`
match at vercel-packages.


---------

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-25 20:13:01 +02:00
Niklas Mischkulnig 49dfc947a0 Turbopack: correctly trace through TypeScript __importStar (#97697)
It was just deopting too early, we don't even need to special case this

No measurable perf impact
before
```
commit 7d29e358c8 (HEAD, tag: v16.4.0-canary.5)
    v16.4.0-canary.5

pnpm next build --experimental-build-mode=compil  400.89s user 51.96s system 747% cpu 1:00.61 total
pnpm next build --experimental-build-mode=compil  394.99s user 54.10s system 757% cpu 59.289 total
pnpm next build --experimental-build-mode=compil  399.87s user 58.71s system 772% cpu 59.398 total
```
after:
```
pnpm next build --experimental-build-mode=compil  396.48s user 58.23s system 760% cpu 59.821 total
pnpm next build --experimental-build-mode=compil  403.07s user 54.17s system 778% cpu 58.716 total
pnpm next build --experimental-build-mode=compil  398.27s user 57.03s system 738% cpu 1:01.61 total
```

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-25 17:17:23 +00:00
Tobias Koppers 331d81fbda [turbopack] simplify ecmascript effect queue (#97771)
### What?

Use a plain stack for ECMAScript analysis effects and move
closure-argument effect extraction into a shared synchronous helper
before the async call handlers run.

### Why?

The effect queue belongs to a single module-analysis task, so mutex
protection is not required for the queue itself. Processing argument
effects before entering the async handlers also removes duplicated
argument-scanning code and callback generic plumbing.

This is an inspection experiment, not a demonstrated performance
improvement. The observed benchmark difference was within noise and was
measured with a debug native binding, so it is not valid evidence for a
micro-optimization.

### How?

The queue remains LIFO: effects are still appended in reverse order and
consumed with `pop()`. The special `Array.prototype.map` scope handling
keeps the same `LeaveScope` ordering. Call and dynamic-import handlers
now receive already-unlinked argument values.

### Verification

- `cargo fmt -- --check`
- `cargo check -p turbopack-ecmascript`
- Not run: clippy, Rust test suite, or integration tests (inspection
experiment)

<!-- 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 19:09:08 +02:00
Aurora Scharff 981dd276ac Update default Create Next App favicon (#97634)
## Summary

Replace the Vercel triangle favicon in all non-empty default Create Next
App templates with the same Next.js favicon used on nextjs.org. This
gives new Next.js apps Next.js branding in the browser tab while leaving
the Vercel deployment call to action unchanged.

## Verification

- `pnpm --filter=create-next-app build`
- Generated App Router and Pages Router starters with `--skip-install`
and verified that their emitted favicon matches
`https://nextjs.org/favicon.ico` across all eight template variants.

<!-- NEXT_JS_LLM -->
2026-08-25 18:45:02 +02:00
next-js-bot[bot] f70c27813b v16.4.0-canary.7 v16.4.0-canary.7 2026-08-25 16:20:13 +00:00
Sebastian "Sebbie" Silbermann 09f9c8a758 Fix ISR misses with backslashes in segments when deployed on Windows (#97876)
https://github.com/vercel/next-js-mirror/pull/150
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 18:12:50 +02:00
Sebastian "Sebbie" Silbermann 7a5937a8ab [next/image]: disable avif image optimization (#97875)
https://github.com/vercel/next-js-mirror/pull/160
2026-08-25 18:12:50 +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 9f7ee4a067 fix(turbopack-trace-utils): skip the ctrl-c handler on wasm (#97856)
> Replaces #97582, 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 (`8f8bc4d0b0`), restored, and needs review
again. Sorry for the churn.

### What?

`turbopack-trace-utils` no longer registers a Ctrl-C handler on wasm
targets.

### Why?

`tokio::signal` does not exist on wasi, so `exit.rs` failed to compile.
There is also no SIGINT to
wait for inside a wasm instance, so there is nothing to handle.

### How?

The `tokio::spawn` calls in `ExitGuard::new` and `ExitHandler::listen`
are gated to non-wasm. On
wasm, `ExitGuard` fires only when the guard is dropped, and
`ExitHandler::listen` registers no
handler — the doc comments now say so, and point wasm callers at
`ExitHandler::new_receiver`.

<!-- 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 0d8814044c refactor(turbopack-cli-utils): replace crossterm with owo-colors (#97855)
> Replaces #97581, 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 (`e1d79b6f4c`), restored, and needs review
again. Sorry for the churn.

### What?

`turbopack-cli-utils` uses `owo-colors` instead of `crossterm` for issue
formatting.

### Why?

Two reasons:

- `crossterm` 0.26 has no wasm backend, and fails to compile for wasm
with 8 errors inside the crate
  itself.
- It was pulling its weight for very little: the only uses were
`StyledContent` and `Stylize` in
`issue.rs`, and `owo-colors` — already a dependency of that same file —
covers all of them.

### How?

Swap the trait usage over and return `String` from `show_all_message`
rather than
`StyledContent<String>`. Rendered output is unchanged.

This also removes `crossterm`, `crossterm_winapi`, `mio 0.8`,
`signal-hook` and `signal-hook-mio`
from `Cargo.lock` (and lets cargo drop the `mio 1.2` disambiguation,
since only one `mio` remains).

<!-- 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:02 +02:00
Tobias Koppers 301195c703 fix(turbo-tasks-fs): create symlinks through the WASI API on wasi (#97854)
> Replaces #97580, 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 (`79642fdafb`), restored, and needs review
again. Sorry for the churn.

### What?

`turbo-tasks-fs` creates symlinks through
`std::os::wasi::fs::symlink_path` when targeting wasi.

### Why?

`std::os::unix` does not exist on wasi, so `disk.rs`'s symlink creation
failed to compile:

```
error[E0433]: cannot find `unix` in `os`
    --> turbopack/crates/turbo-tasks-fs/src/disk.rs:1375
```

### How?

A `cfg(target_os = "wasi")` arm calls the WASI equivalent, which takes
the same `(target, link)`
argument order. That API is still behind the unstable `wasi_ext`
feature, enabled for the wasi target
only via `cfg_attr`. The Windows junction-point path and the unix path
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:02 +02:00
Tobias Koppers 362b1efe98 fix(turbo-tasks): compile EventListener::wait on wasm (#97853)
> Replaces #97579, 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 (`1d868fd710`), restored, and needs review
again. Sorry for the churn.

### What?

Gives `turbo_tasks::EventListener::wait` a working blocking
implementation on wasm targets, instead
of failing to compile there.

Also moves `criterion` to a non-wasm dev-dependency, which is what makes
`cargo test -p turbo-tasks --lib --target wasm32-wasip1-threads`
buildable at all — see *How?*.

### Why?

`event-listener` gates its blocking `wait()` / `wait_timeout()` /
`wait_deadline()` behind
`#[cfg(all(feature = "std", not(target_family = "wasm")))]`, so they are
unavailable on **every**
wasm target — even `wasm32-wasip1-threads`, which has real threads that
can park. That made
`EventListener::wait` fail to compile.

This is load-bearing rather than a corner case: `wait()` backs the
priority runner's task barriers
(six call sites in
`turbopack/crates/turbo-tasks/src/priority_runner.rs`) and
`turbopack/crates/turbo-tasks/src/scope.rs`. Anything that merely
compiled while `wait()` was
unusable would be hollow.

### How?

`event-listener`'s own native implementation (`wait_internal` →
`wait_with_parker`) is just "register
an unparker as the listener's task, then `Parker::park()` in a loop".
`EventListener` already
implements `Future`, so on wasm the same thing is expressed one level
up:

```rust
fn block_on_listener(listener: event_listener::EventListener) {
    #[cfg(not(target_family = "wasm"))]
    { use event_listener::Listener as _; listener.wait(); }
    #[cfg(target_family = "wasm")]
    futures::executor::block_on(listener);
}
```

`futures` is already a dependency of this crate, `executor` is one of
its default features, and
`futures-executor` is already in the wasm dependency graph, so nothing
new is pulled in. Native
targets keep calling `event-listener` directly, so their behaviour and
fast path are untouched.

Both `wait()` implementations — the default one and the
`hanging_detection` one — route through that
single helper, so the two cannot drift apart and the target `cfg` exists
in exactly one place.

**Verification.** A new test registers a listener, spawns a thread that
sleeps before notifying, then
waits — so the listener parks first and the parking path is exercised
rather than the
already-notified fast path. It asserts both that the notifier ran *and*
that time actually elapsed,
so a `wait()` that returned early fails instead of silently passing.

It passes natively, and the wasm code path was executed under wasmtime
with real wasi threads on
`wasm32-wasip1-threads`: the waiter blocked **300.9 ms** against a 300
ms notifier delay and was
woken by the other thread. As a control, a deliberately non-blocking
variant returns in ~1 µs and
fails the assertions.

Two notes on the surrounding tooling:

- `criterion` is used only by the `[[bench]]` targets, but it pulls in
`rayon`, which refuses to
compile for wasi, and cargo resolves dev-dependencies even for `cargo
test --lib`. Gating it to
non-wasm targets is semantically right (the benchmarks don't run on
wasm) and unblocks wasm test
  binaries for this crate generally. Native benchmarks are unaffected.
- The test binary now *builds* for wasm but cannot be instantiated by a
plain runtime, because
`turbo-tasks` imports `env.read_custom_section` for its link-time task
registries and no CLI
runtime provides it. That hook is tracked separately; until it exists,
the runtime evidence above
comes from a standalone binary that copies the helper's wasm body
verbatim against the same
  `event-listener` and `futures` versions.

<!-- 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>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
2026-08-25 14:13:56 +02:00
Tobias Koppers ab0b23f065 fix(turbo-rcstr): allow the napi feature on wasm targets (#97852)
> Replaces #97578, 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 (`0b53306e79`), restored, and needs review
again. Sorry for the churn.

### What?

Removes `turbo-rcstr`'s hard stop against combining the `napi` feature
with wasm targets, and moves
the `napi` dependency out of the non-wasm dependency table.

### Why?

The crate contained:

```rust
#[cfg(all(feature = "napi", target_family = "wasm"))]
compile_error!("The napi feature cannot be enabled for wasm targets");
```

This predates napi v3, which supports `wasm32-wasip1-threads`. With the
assertion removed,
`turbo-rcstr/napi` and everything built on it —
`turbopack-node/worker_pool`,
`next-api/worker_pool`, `next-napi-bindings` — compiles for wasm.

### How?

Delete the `compile_error!`, drop the now-redundant `cfg` on the
`napi_impl` module, and declare
`napi` as a normal optional dependency rather than a non-wasm-only one.

<!-- 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 14:13:56 +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