Skip to content

[pull] canary from vercel:canary - #1342

Merged
pull[bot] merged 4 commits into
code:canaryfrom
vercel:canary
Aug 26, 2026
Merged

[pull] canary from vercel:canary#1342
pull[bot] merged 4 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Aug 26, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

mischnic and others added 4 commits August 26, 2026 14:01
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:
rust-lang/rust#156749
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.
### 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>
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)
@pull pull Bot locked and limited conversation to collaborators Aug 26, 2026
@pull pull Bot added the ⤵️ pull label Aug 26, 2026
@pull
pull Bot merged commit d352f17 into code:canary Aug 26, 2026
15 of 20 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants