Skip to content

Sync Rspack with React main and webpack cancellation handling - #2

Open
nikhilsnayak wants to merge 66 commits into
SyMind:react-server-dom-rspackfrom
nikhilsnayak:refresh/react-server-dom-rspack-main
Open

nikhilsnayak wants to merge 66 commits into
SyMind:react-server-dom-rspackfrom
nikhilsnayak:refresh/react-server-dom-rspack-main

Conversation

@nikhilsnayak

Copy link
Copy Markdown

Summary

Merge React main (00f48cecf) into react-server-dom-rspack and align the Rspack adapters with webpack's cancellation handling: pass the signal to processReply and use attachAbortSignal for render/prerender requests. Add the missing lock entry for the existing resolve.exports dependency.

How did you test this change?

  • Rspack source tests: 31 passed, 8 skipped; Jest reported a worker teardown warning.
  • Package build and all 8 built-package tests passed.
  • ERSC against the rebuilt packages: build/check passed, 365 unit tests and 106 browser tests passed (14 skipped).
  • Flow timed out in shared ReactFlightClient.js, also reproduced with the webpack baseline. An upstream webpack composite-signal test fails under the local Node 24/Jest AbortController setup.

ronnakamoto and others added 30 commits August 11, 2026 11:40
…#34983)

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
…act#37160)

Removing an unregistered listener used splice(-1), which deleted the
last tracked entry and left the real handler stuck on DOM children.
focus() passes through portals as it attempts focus down the fiber tree.
blur() exited early based on a containment check, causing it to stop at
portals.

The result could be a focused element that cannot be blurred.

Follow up to react#37125, which made
blur() apply recursively to be consistent with focus().
Fixes a bug where `compareDocumentPosition` fiber traversal would stop
searching after an empty Fragment fiber. Also affects `scrollIntoView`
…pty portals (react#37163)

Accept documentElement in the CONTAINS fiber fallback when there is no
React fiber, and position empty portaled fragments against the portal
container instead of the React host parent.
…ct#37164)

Commit and delete previously skipped TEXT_NODE, so listeners added
before a text child mounted never reached it. Match addEventListener.
…act#37165)

dispatchEvent appends a temporary Text node to the fragment's nearest
host parent, but a Document can't contain Text, so createRoot(document)
threw HierarchyRequestError whenever the fragment had a listener or the
event didn't bubble.

Use a Comment node for Document containers: it is a legal document child
and sits at the fragment's own position, unlike documentElement, which
would put the target inside the fragment and fire its listeners twice.
react#37166)

Commit-time fragment ancestry stopped at HostPortal via isHostParent, so
a child added after listeners were registered never received them even
though existing portal siblings did. Collect fragment parents past
portals and other non-HostComponent host parents so commit bookkeeping
matches HostComponent/HostRoot fragment ancestry.
react#37167)

Just a small refactor to consolidate FragmentInstance helpers into one
module
)

There was inconsistent behavior with text nodes retaining event
listeners while all other nodes have them removed in deletion effects.
This fixes that handling
…eact#37169)

`{once: true}` events are supposed to fire once. Since the
`addEventListener` implementation adds a listener to each host child,
`once` was not respected if you trigger event on multiple children.

Here we wrap the event so we can remove is after the first call
Reverse react#35709. Turn the flag on in the OSS/canary default and in every
hardcoded fork, including the native (RN) forks, and keep the test
renderers in sync. www stays GK-driven, so `www.js` and `www-dynamic.js`
are unchanged.

Leaving the flag in the repo for now; this just flips the default on.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code (Opus 5) <noreply@anthropic.com>
…er (react#37315)

Every server entry point that accepts a `signal` attached an abort
listener to it and only ever removed that listener from inside the
listener itself. On the success path the signal never aborts, so the
listener stayed attached and its closure kept the whole `Request`, and
therefore the entire rendered output, reachable for as long as the
caller's signal lived.

This matters most for composite signals from `AbortSignal.any()` and for
timeout signals, because the runtime retains those for as long as they
carry a non-weak abort listener, and releases them only when the last
listener is removed or the signal aborts. A composite passed to
`prerender()` therefore became a garbage collection root holding a
finished render for the lifetime of the process. A plain
`AbortController` signal is never retained that way, but it still keeps
the render reachable for as long as the caller holds the controller.

Each listener is now bound to a lifetime signal passed to
`addEventListener`, so the runtime removes the listener as soon as that
signal aborts and nothing has to track a teardown function. Flight
reuses `request.cacheController`, which already aborts on a fatal error,
at the completion of the flush loop (depends on react#37342), and in
`abort()`. Fizz has no equivalent, so it gains a
`renderLifetimeController` that aborts at those same three points.
`processReply` creates its controller only when a caller passes a
signal, so a reply without one allocates nothing. Since `abort()`
returns early once the request is past `OPEN`, removing the listener at
those points cannot change observable behavior. The fifty-two copies of
the listener block across the entry points collapse to a single
`attachAbortSignal` call each.

Binding the listener to the render also covers a cancelled stream, which
calls `abort()` without the request ever reaching a terminal status, so
a teardown driven by that status would have left the listener attached.
Fizz ends the lifetime in `fatalError` rather than at the `CLOSING` to
`CLOSED` transition, because a shell error rejects before the caller
receives a stream. Nothing then consumes the request, it never closes,
and a listener waiting for that transition would never come off.

The two new controllers are aborted with an explicit reason. A call to
`abort()` without one constructs an `AbortError` DOMException. Capturing
the stack trace dominates that cost, and the cost grows with the depth
of the stack, so every render and every reply would pay for an object
that no code reads.

`processReply` no longer returns its `abort` function, because that
return value existed only so each `encodeReply` implementation could
wire the signal up itself, and nothing uses it now that the wiring lives
inside. A reply whose model settles synchronously gets no listener,
since aborting it was already a no-op.

The tests assert on the lifetime signal, because the runtime's removal
does not go through `removeEventListener` and is therefore invisible to
a patched signal. `ReactFlightDOMNode-test` asserts the removal itself
with `getEventListeners` from `node:events`, which jsdom has no
equivalent for.

Two cases stay open. A request whose stream is neither consumed nor
cancelled never ends, and a reply with a part that never settles never
settles either, so both keep their listener.
…t#37342)

A debug channel with a readable side lets the client fetch debug objects
lazily. For example, React serializes each component's props into the
debug model and defers the part of an object tree that exceeds the
model's object limit. A deferred object stays retained, and its debug
chunk stays pending, until the client asks for it or the channel closes.
The render can therefore complete while such an object is still
outstanding.

When that happens, `flushCompletedChunks` closes the main stream, sets
the request status to `CLOSED`, and returns before it reaches the block
that releases the render's resources. Once the client closes the debug
channel and the retained objects drop, a later flush does reach that
block, but the cache controller is only aborted while the status is
below `ABORTING`, and `CLOSED` is above it. The signal is therefore
never aborted at all rather than merely released late, so anything that
waits on `cacheSignal()` to clean up resources waits for the lifetime of
the process.

This change moves the cache controller abort above the debug stream
bookkeeping. An empty pending chunk count already means the render is
complete, and debug chunks carry development-only instrumentation rather
than the render's output, so the cache signal can abort at that point no
matter what the debug stream is still doing. The path that writes debug
chunks on the main stream can now reach this code on several flushes,
which is safe because aborting an aborted controller does nothing a
second time.

The taint queue cleanup stays where it is. A tainted typed array,
`DataView` or blob is checked against the taint registry as its chunk is
written, and such a write can happen long after the render completes,
either because the client queried a deferred debug object or because a
blob's stream resolved late. Moving it up would let those writes through
unchecked, and it would fix nothing, because unlike the abort it never
sat behind the status guard.
The build size comparison comment was posted by Danger, which
authenticated with a personal access token hardcoded in
`scripts/tasks/danger.js`. That token has since been revoked, so sizebot
has been posting nothing at all (due to e.g.
https://github.com/react/react/actions/runs/32181295467/job/95855395224?pr=37315).
This change rebuilds it on the short-lived `GITHUB_TOKEN` that Actions
mints per run and a new workflow only responsible for rendering
untrusted JSON input as markdown in a PR comment.

A straight token swap would not have worked. Fork pull requests did
receive sizebot comments, but only because the token was in checked-out
source: the sizebot job runs on the `pull_request` trigger, where a
fork's `GITHUB_TOKEN` is read-only and cannot comment. The comment
therefore moves to a new `workflow_run` workflow,
`runtime_sizebot_comment.yml`, which runs in this repository with a
writable token no matter where the pull request came from. It posts a
placeholder when a build is requested and rewrites it in place when the
build completes, fails, is cancelled, or is held for maintainer
approval.

The measurement stays on the unprivileged side of that boundary which
are recorded as raw sizes into a `sizebot-results` artifact, and the new
workflow downloads only that JSON and renders it from a default-branch
checkout. The job holding `pull-requests: write` never unpacks a build
produced by a fork, which matters because the existing base-build
download justifies using an unverified artifact on the grounds that the
job has restricted permissions. Thresholds, the critical bundle list,
and the comment template all live on the trusted side, and the renderer
validates every field it reads out of the artifact so that a crafted
build path cannot inject markdown. The pull request number is resolved
from the API rather than from the artifact, since a number read from
fork-controlled data would let any contributor post a bot comment on an
arbitrary pull request.

Resolving that number needs a branch lookup rather than any of the
obvious approaches. `workflow_run.pull_requests` is empty for fork runs,
and neither `commits/{sha}/pulls` nor the search API indexes fork pull
request head commits, so the workflow looks the pull request up by
`owner:ref` instead.

A comment is only ever left alone in one situation: when it already
describes the pull request's current head and the event being handled
belongs to an older commit. Everything else is written, and marked stale
whenever the report does not describe the current head. That single rule
covers both an old run finishing after a force push and a new build
superseding a report already on display, and in the latter case the
previous numbers stay visible instead of being blanked back to a
placeholder.

The results file carries a `version` field. Its writer is whatever
`compare-sizes.js` a pull request branch happens to carry, while its
reader is on the default branch, so the two can mismatch and the
renderer needs to be able to say so instead of misrendering a table.

Porting the table fixed a longstanding bug in `change()`. Testing
`decimal < 0.0001` reported every size decrease as unchanged, which is
why `signDisplay: 'exceptZero'` never had a negative number to render: a
709.04 kB to 708.68 kB drop printed as `=`. It now compares the
magnitude.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
Flight has two ways to write a string:

1. Small ones are inlined into the JSON model.
2. Large ones (>= 1024 chars) are outlined into a binary text row so
they don't get double-encoded and double-parsed.

Neither is ever deduplicated. That's most visible in client reference
metadata, where a route repeats the same bundler chunk URLs across every
client reference (for example,
vercel/next.js#95559).

**This PR adds a dedupe map for strings inside import metadata.** A
string is written once into its own row, and every occurrence is a
reference to it.

## How it works

When we're about to write a string in import metadata at least as long
as the threshold, we look it up in the request's map:

- **If it's not in the map:** Emit a row containing the string, store
that row's reference in the map, and write the reference here.
- **If it is:** Write the reference.

So every string goes on the wire once, and every occurrence costs a few
bytes. An earlier version waited for the second occurrence before
outlining, which is the right default for arbitrary strings where most
never repeat (it's what react#27537 does for objects). Import metadata is the
opposite case: a chunk is listed by every client module that lives in
it, so a chunk name that appears once is the exception. In the bench
app's three routes, every chunk string at least 16 characters long
appears more than 20 times and none appears once. Outlining on first
sight saves the inline copy, and a string that never repeats costs 7
bytes more than inlining it.

Import metadata needs its own map and its own queue. The client resolves
a client reference as soon as it parses the import row, and import
chunks flush ahead of model rows, so the string row has to be in the
same queue to arrive first.

The client needs no protocol changes. It already resolves `$N`
references, and a row holding a string resolves to that string. It does
get a check that import metadata never blocks on a row that hasn't
arrived, which is the other end of the queue ordering above, and
`getOutlinedModel` stops allocating a path array for references without
one.

Model strings are left alone. An earlier version of this PR deduped them
too, but we're not going to do this now per review (maybe in a
follow-up). Metadata on the debug channel is also left alone: it's a
separate serialization path, and deduping across the two would make the
main payload depend on whether a debug channel is attached.

## Threshold

The trigger is 16, low compared to what a model-side threshold would
want, because import metadata is repetitive but its parts are short. How
much this saves depends on how many client references share a chunk
list, so measuring one string on its own is misleading. For a chunk path
of realistic length today:

| references sharing the chunk | before | after |
|---|---|---|
| 1 | 80 B | 87 B |
| 2 | 160 B | 122 B |
| 3 | 240 B | 157 B |
| 5 | 401 B | 228 B |
| 10 | 813 B | 416 B |
| 40 | 3273 B | 1526 B |
| 80 | 6594 B | 3047 B |

(Import and string rows only.) It costs 7 bytes at 1 reference and wins
from 2. Chunk paths in this app are 47 characters, so a threshold of 48
or higher saves nothing at all here. That's why it's 16: picking a
number just under one bundler's path length gives you something that
quietly stops working on the next bundler.

The map is bounded by the combined length of the strings it holds, 32
KiB. Once the budget is spent, new strings are written inline every time
while strings already outlined keep deduping. That makes the savings
depend on the order strings are first seen: a shared chunk URL first
encountered after 32 KiB of unique module ids won't be deduped. That's
main's behavior, so it's a missed win rather than a regression, but a
manifest-heavy dev route could hit it.

## Byte measurements

Three routes of a Next.js app, serial requests:

| route | Flight | document | document (gzip) |
|---|---|---|---|
| `/dashboard` | −48.4% (710.1 → 366.5 KB) | −34.3% | −7.8% |
| `/docs` | −5.8% (555.2 → 523.1 KB) | −5.0% | −0.7% |
| `/blog` | −5.4% (878.8 → 831.7 KB) | −4.4% | −1.7% |

The difference between the routes is how many client references each one
has. On `/dashboard` the import rows shrink from 388.0 KB to about 32 KB
with the row *count* unchanged at 114, because every client reference
repeats the same 49 chunk URLs.

gzip already collapses repeated strings, so −48.4% raw is only −7.8%
compressed. The bytes still have to be escaped, encoded and copied
before they reach the compressor, which is where most of the speedup
below comes from.

## Speed measurements

Benchmarked end-to-end through a Next.js app on Vercel Sandbox VMs (x86
Xeon), 16 boots, paired ABBA within each boot, boot as the unit of
replication. Base is the merge-base with main, `eafeac09`; candidate is
the current head, `e0b4614c`.

| cell | effect | 95% CI | p |
|---|---|---|---|
| `/dashboard` serial req/s | **+16.9%** | ±1.7 | <0.0001 |
| `/dashboard` serial p95 latency | −18.2% | ±2.4 | <0.0001 |
| `/dashboard` serial TTFB | −23.2% | ±1.2 | <0.0001 |
| `/dashboard` under load req/s | **+17.0%** | ±3.4 | <0.0001 |
| `/dashboard` under load median latency | −13.9% | ±2.5 | <0.0001 |
| `/docs` serial req/s | +3.0% | ±1.4 | 0.0003 |
| `/docs` serial TTFB | −3.1% | ±1.0 | <0.0001 |
| `/blog` serial req/s | +2.4% | ±1.0 | 0.0001 |
| `/blog` serial median latency | −2.4% | ±0.7 | <0.0001 |

No detected difference: `/blog` and `/docs` under load (p=0.13–0.56).
All 16 boots are positive on both `/dashboard` cells. The `/dashboard`
headline has now been measured in four separate 16-boot runs across four
heads of this branch and is p<0.0001 in each; the small routes cleared
p<0.01 only on this head, after the serializer change below, having sat
at p=0.02–0.06 on the three earlier heads.

The previous head, `4569e1d6`, which outlined on the second occurrence
rather than the first, measured +14.9% ±2.3 on `/dashboard` serial req/s
and −17.3% ±5.7 on TTFB against the same base. Those intervals overlap
the ones above, so the switch is not a measurable speedup on its own;
the bytes it saves are about 1% of the payload.

In a real browser on `/dashboard` (measured on an earlier commit of this
branch, `aed4d523`), hydration is −2.8% ±1.3 (p=0.0003) / −2.4% ±0.9
(p=0.0001) and LCP is −5.3% ±2.0 (p<0.0001) / −3.6% ±2.6 (p=0.009).
Client navigation is under the noise floor in both.

### Where the time goes

32 CPU profiles, taken after the timed runs with an identical request
count in both arms, so absolute sampled milliseconds are comparable. One
pass per boot, no replication statistics — directional, not a claim.
These profiles are from `aed4d523`. The current head also walks the
metadata into a copy before a plain `stringify`, after a detour through
a `stringify` replacer that measured 2.3× slower in isolation (a
replacer function takes V8 off its fast path for the whole call); the
`transformImportMetadata` frame below is a fair proxy for the current
cost.

Cheaper:

| base | candidate | frame |
| ---: | ---: | --- |
| 23.6 s | 5.7 s | ReactDOM `preinitScript` |
| 24.9 s | 10.6 s | `serializeClientReference` |
| 81.8 s | 67.3 s | `utf8Write` |
| 93.9 s | 80.8 s | `createFromString` |
| 50.7 s | 39.2 s | Next's `htmlEscapeJsonString` |

More expensive:

| base | candidate | frame |
| ---: | ---: | --- |
| 0 | 9.3 s | `transformImportMetadata` |
| 2.0 s | 10.3 s | `getOutlinedModel` (SSR-side Flight client) |
| 7.7 s | 11.7 s | `parseModelString` |
| 154.8 s | 158.2 s | `resolveModelToJSON` |

About +31 s of new work against −79 s inside the runtime bundle and −45
s in node's buffer and string layer.

`getOutlinedModel` resolving references is the mechanism working, not a
warning sign. Next.js runs a Flight client on the server to read its own
payload, and a `/dashboard` payload goes from 0 references inside import
rows to 4964, so a frame that barely ran before now runs once per
reference. Each call is a lookup on a row that has already been
initialized: the string row goes into the import queue ahead of the
import row that reads it, so it has always arrived and nothing blocks.
`parseModelString` grows for the same reason.

`preinitScript` doesn't get cheaper from writing fewer bytes. It does
two dictionary lookups keyed by the chunk URL per call, and the call
count and argument values are unchanged — the resolved models are
identical. What changes is string identity: in the base build every one
of the 5013 chunk-URL occurrences is a fresh string out of `JSON.parse`
whose hash has to be computed before the lookup, and with dedupe the 49
distinct URLs are parsed once and every reference yields the same
string, so V8's cached hash makes the repeat lookups nearly free. Some
of the `htmlEscapeJsonString` and buffer-layer drops have the same
cause.

### React-level CPU in isolation

The e2e numbers above include everything downstream of React (escaping,
encoding, compression, the SSR client). To see React's own serialization
cost, 114 import rows of dashboard-shaped metadata (49 shared
74-character chunk names per row) were rendered against one request on
the production bundles with a no-op destination, arms interleaved,
median of 5 rounds × 200:

| | main | this PR |
|---|---|---|
| 49 names shared by all rows | 0.502 ms | **0.322 ms** (−36%) |
| 5586 unique names, nothing to dedupe | 0.477 ms | 0.771 ms (+62%) |

The second row is the worst case for this change, a manifest where every
chunk name appears once. It costs about 40 ns per unique string, plus
about 130 ns for each row the budget lets it outline, against a payload
that is otherwise unchanged.

The metadata is serialized by copying it with the strings already
replaced and then calling plain `JSON.stringify`; a `stringify` replacer
function would keep V8 off its fast path for the whole call (measured
2.3× slower than plain in isolation, even writing a sixth of the bytes).
The copy covers plain JSON only and falls back to the replacer for
anything else (`toJSON`, class instances, keys that exist on
`Object.prototype`, depth over four, which is how cycles end up throwing
stringify's own error). Equivalence of the two paths was checked by a
harness that runs both on identical requests and compares the JSON and
the resulting request state: 2,656,142 cases, including exhaustive
enumeration of small trees over adversarial atoms, 100k seeded random
values, and the cases from two independent adversarial reviews — 0
divergences outside four stated assumptions that no bundler manifest
violates (no Proxies, no index accessors polluted onto
`Array.prototype`, no primitive wrappers with a swapped prototype,
side-effect-free property access).

## Cost where there's nothing to dedupe

React's own `flight-ssr-bench` fixture has about ten client modules and
no repeated chunk paths, so the dedupe never fires and the change can
only cost. It costs a little, if anything. Over 16 boots at `aed4d523`
the Flight+Fizz Node sync variant was +0.9% ±0.7 on median inject time
(p=0.008), worse on 14 of 16 boots. On the current head the four
Flight+Fizz inject cells are between +0.4% and +0.8% on the median, none
below p=0.07; across all 88 fixture metrics (Fizz and Flight+Fizz, Node
and Edge, sync and async, inject and HTTP at c=1/c=10) nothing reaches
p<0.01 and `heapMb` is flat to ±0.1%. So the no-dedupe cost is somewhere
around half a percent of inject time on this fixture, at the edge of
what it can resolve.

I couldn't localize it past that. It isn't the per-request `Map`, which
is about 22 ns against a 14 ms render, and it isn't allocation — `gcMs`
and `heapMb` are flat. Using the Fizz-only variants as a within-boot
control, since nothing in `ReactFlightServer.js` can reach them, the
Flight-specific residual on that cell is +0.8% ±0.5 and the other three
variants scatter around zero (+0.4%, +0.1%, −0.2%). A build that
re-inlines `escapeStringValue` back into the string branch, which is the
only change here that runs for every string in the model rather than
only for import metadata, doesn't recover it either (+0.2% ±0.3 on the
same cell, another 16 boots). So this looks like code layout rather than
a specific added operation, and it's near the resolution limit of the
fixture.

<details>
<summary>Verification</summary>

- Both arms' payloads for `/dashboard` were parsed and their
`$`-references resolved recursively, then deep-compared: the resolved
models are identical. The 49 extra model rows are exactly the 49
distinct chunk URLs. All 114 import rows match after resolution.
- Arms fingerprint distinctly (`a898f40a7bbd` vs `87fb4b7ba15e`), so the
two builds are genuinely different.
- Build fingerprints differ between arms (`04440a11435d` vs
`43d09027ce58`) and the arm version strings carry the expected shas.
- Per-boot deltas are printed by the harness; on `/dashboard` serial
req/s all 16 boots are positive (range +11.2% to +22.9%).
- The bench fixture sets a deployment id, so every chunk URL carries a
`?dpl=` query param that exactly doubles its length (74 chars vs 37). An
app without one would see roughly half the absolute byte saving on this
route. The CPU wins that come from string identity rather than byte
count should degrade less than proportionally, but that wasn't measured.
- Not measured: payloads that exceed the 32 KiB tracking budget, and
whether 16 is optimal rather than merely low enough.

</details>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`build_and_lint` assigns its `[bundle, bundleType]` pairs to 25 workers
per channel by round-robin, which leaves the slowest worker with 56-62
seconds of rollup time while the mean worker has 40-42 seconds (measured
from the timestamped `BUILDING`/`COMPLETE` lines in recent `main` run
logs).

This change shards by measured build time instead, and the measurement
maintains itself:

`yarn build` writes the timing results into `build/__shard_timings__/`,
which rides along inside the existing per-worker artifacts.

`process_artifacts_combined`, which already downloads all 50 artifacts
and is off the critical path, combines them into `build-weights.json`
and saves it to the actions cache under a per-run key. Readers restore
the most recent entry via a `restore-keys` prefix. Only pushes can save
to cache keys that PRs can read, so pull request runs benefit from the
weights but cannot poison them.

The new measurement is always written verbatim rather than merged with
previous weights, so removed bundles drop out instead of accumulating.

A per-bundle diff against the previous weights is logged so that we can
monitor whether single-run variance is too high, in which case shards
should be determined from timings across the last N runs instead. Even
on this PR a perfect prediction would've only gained us ?s for the
slowest shard.



Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
…act#37225)

## Summary

By replicating the `IndexSet` behavior with a simple inline array, this
is able to avoid any heap allocation / memory thrash for
`AbstractValue`. It also reduces the size of AbstractValue from 72 bytes
+ all of the heap allocations, to only 18 bytes.

## How did you test this change?

Ran all of the fixtures to confirm byte output is identical.

Effects on memory and compile time:

| Benchmark | Peak allocation | Allocation count | Wall time |

|------------------|-----------------------------|------------------|-----------|
| legacy/image.tsx | 58.21 -> 33.40 MiB (-42.6%) | -51.1% | -39.5% |
| next-client | 58.21 -> 33.40 (-42.6%) | -40.5% | -25.1% |
| devtools | 26.39 -> 16.29 (-38.3%) | -26.7% | -14.8% |
| fixtures | 17.35 → 9.41 (-45.8%) | -9.9% | -9.0% |
)

## Summary

In the mutation / aliasing inference, the state is used twice, and both
get cloned. Only one clone is necessary, the second can be safely moved.

This has minimal impact on peak memory usage, but it does reduce wall
time by *10%* on real Next.js benchmark apps.

## How did you test this change?

Ran against benchmark apps to confirm identical byte output.
Ran against the 1800+ Rust Compiler test fixtures.
…t#37206)

tl;dr this reduces peak memory allocation by 5-15%, and reduces codegen
time by 30-70% depending on payload. On heavy components with deep ASTs
the impact is more exaggerated.

## Summary

Codegen of temp vars records the expressions they replaced, so that they
can be unwound. In the TS version this uses a `Map` and stores pointers
to AST nodes - relatively cheap. For borrow-checking reasons, the Rust
version clones the AST. This results in recurring deep clones, making
codegen accidentally quadratic and using significant amounts of memory.

This introduces a convenience data structure for emitting temp vars in
an unwindable manner, without heavy AST allocation.

It also avoids a separate AST deep clone when propagating null values.

## How did you test this change?

All fixtures pass with byte-identical outputs.

Ran this against real codebases and pathological benchmark cases,
confirming byte-identical output as well.
react#37357)

This follows react#37315, which added the render lifetime controller to bound
the abort listener that `attachAbortSignal` attaches to a caller's
signal. `RequestInstance` constructed one for every request, so a render
that is given no signal allocated a controller, aborted it on
completion, and nothing ever observed either.

The controller is now created in `attachAbortSignal`, and the three
places that end the lifetime go through `endRenderLifetime`, which does
nothing when there is no controller. Callers that pass a signal are
unaffected. Callers that do not no longer allocate one, and `signal` is
optional in every browser, edge and static entry point, while
`renderToPipeableStream` and `resumeToPipeableStream` accept no signal
at all.

They also no longer reach `AbortController` at all, which matters more
than the allocation. Fizz had no runtime dependency on it before react#37315,
and an unconditional one reaches environments that provide the API
through a polyfill. An incomplete polyfill can then fail a render that
never asked for abort support.

The new test asserts that no controller is constructed when no signal is
passed. It fails with the eager construction restored, since nothing
else in the suite would notice a regression to it.
…t#37232)

## Summary

Propagate errors returned while lowering block statements instead of
discarding them and continuing with partially built HIR.

The original issue was triggered by the SWC path, where a TypeScript
`this` pseudo-parameter remains in the AST but is omitted from
`ScopeInfo`. Lowering rejects the AST parameter, but the function-body
block wrapper previously swallowed that error and returned a partial
function.

```ts
function Component() {
  useEffect(() => {
    const get = (): Val => {
      window.value = {
        count: 0,
        method(this: Val) {},
      };
      return window.value;
    };
    get().count++;
  }, []);
}
```

This could emit a partial transform with the assignment removed:

```js
function Component() {
  useEffect(() => {
    const get = () => {
      return window.value;
    };
    get().count++;
  }, []);
}
```

## How did you test this change?

Added a [source-level reproduction against the SWC
adapter](https://github.com/wbinnssmith/swc/blob/114e9c55b2/crates/swc_ecma_react_compiler/src/tests/integration.rs#L1563-L1591)
using the same case above.

- With the current lowering crate, the test fails because the adapter
emits a partial program.
- With this change patched into the lowering dependency, the test passes
because compilation bails out.
- `cargo test --manifest-path compiler/Cargo.toml -p
react_compiler_lowering`
…ct#37350)

Every downstream job in `runtime_build_and_test.yml` restored the 50
`_build_*` artifacts with `actions/download-artifact` only after
setup-node, the node_modules cache restore, and any installs had
completed, even though the download is independent of all of them.

This change marks the download as a [background
step](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsbackground)
started immediately after checkout, and adds an explicit `wait:
download_build` before the first step that reads `build/`.

The download starts after checkout because `actions/checkout` runs `git
clean`, which would wipe a previously downloaded `build/` directory. The
`sizebot` job is unchanged because its base-build download also writes
`./build` and would collide with a concurrent artifact restore.

This only shaves of a few seconds from wall time. It's more about
establishing precedent.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
…7351)

In `build_and_lint`, `actions/setup-java` ran sequentially between
setup-node and the node_modules cache restore, but Java is only needed
by `yarn build` for the Closure Compiler bundles. This change marks the
setup-java step as a background step.

Setting up Java is mostly network (download) and CPU (unpack). It
overlaps with installing/restoring node_modules which is network and FS
work. So we aren't competing for resources that would make concurrently
running steps moot.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
…ical (react#37373)

The build workers can restore different weights if they don't restore
the cache at the exact same time. The more time difference, the more
likely they restore different weights which could lead to some bundles
not being built at all (e.g.
https://github.com/react/react/actions/runs/32822851267).



A new job now restores the latest entry once per run and republishes it
as a per-run artifact. The new job sits adds no wall time because it
runs in parallel with `runtime_compiler_node_modules_cache`, which
already gates the build workers and takes about 30 seconds on a cache
hit, while the resolve job does strictly less work (no checkout, no Node
setup, a 5KB cache entry instead of the node_modules restore), so it
finishes first and the build workers start at the same time as before.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
…eact#37379)

Test files running in the Node.js Jest environment share the worker
process's `performance` object, because [`jest-environment-node`
installs it by
reference](https://github.com/jestjs/jest/blob/v29.7.0/packages/jest-environment-node/src/index.ts#L85-L103)
rather than by copy. When a test mocked the clock with
`Object.defineProperty(performance, 'now', ...)`, the mutation hit the
shared object and was never undone, since Jest only restores
`jest.spyOn` mocks when a file's runtime is torn down ([`jest-runtime`'s
`teardown()` calls
`restoreAllMocks()`](https://github.com/jestjs/jest/blob/v29.7.0/packages/jest-runtime/src/index.ts#L1358-L1359)).
Every subsequent test file in the same worker then observed the fake
clock, including jsdom-based files, whose [`performance.now()` subtracts
a window-creation timestamp from the shared object's
`now()`](https://github.com/jsdom/jsdom/blob/v22.1.0/lib/jsdom/living/hr-time/Performance-impl.js#L13-L14).

This change switches the six affected test files to
`jest.spyOn(performance, 'now')` and `jest.spyOn(performance,
'timeOrigin', 'get')`, which Jest restores automatically at teardown.
`ReactFlightDOMEdge-test.js` runs in jsdom and therefore did not leak,
but it used the same pattern and is converted for consistency.

This change is mostly for test hygiene. Was discovered while
investigating a flaky `{"time":NaN}` serialisation bug (e.g.
https://github.com/react/react/actions/runs/32878999825/job/97903912119)

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
… false` (react#37251)

`FragmentInstance` tracks its event listeners so they can be applied to
children added later, and matches them by a normalized options identity.
Omitted options currently normalize to a different identity than an
explicit `false` or `{capture: false}`, even though both mean `capture:
false` per the `EventTarget` contract, where listener identity is the
tuple of type, callback, and capture flag. As a result, a listener added
without an options argument cannot be removed with an explicit
capture-false value (or the reverse).

This change normalizes omitted options to the same capture-false
identity as `false` and `{capture: false}`. The first commit adds a test
to the FragmentRef suite characterizing the current behavior; the second
commit contains the fix and the updated assertions.

---------

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
…ges (react#37384)

The custom `toThrow` override in `scripts/jest/matchers/toThrow.js`
wrapped the built-in matcher to rewrite the pre-Node-17 V8 error message
format ("Cannot read property 'x' of undefined") into the modern one
("Cannot read properties of undefined (reading 'x')"), so the test suite
could run on Node 12 to 16.

On the Node versions this repo runs on (20 per `.nvmrc`, 24 in CI), V8
only ever produces the modern format, so the override is a passthrough.

Mostly removing this because the custom matcher deep-imports
`expect/build/toThrowMatchers`, which no longer resolves on Jest 30
because each Jest package is now bundled into a single file, so this
removal unblocks the Jest 30 upgrade stacked on top.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
eps1lon and others added 30 commits September 1, 2026 20:45
…7475)

## Summary

Alternative to react#37280 that keeps the child-set assertion and instead
fixes the root cause.

The assertion "The children should not have changed if we pass in the
same set." fired while DevTools reconciled the hidden content tree of a
Suspense boundary that had just switched to its fallback.

`updateSuspenseChildrenRecursively` reconciles the content and the
fallback in two passes, but the previous-set lockstep pointer of the
content pass is not bounded. When a boundary is suspended on both sides
of a commit, the pointer advances from the previous content Offscreen
onto the previous fallback fragment, and the leftover-children check
reports `ShouldResetChildren` even though the fallback is reconciled in
the second pass by design.

For a boundary that is filtered from the tree, that flag propagates to
the parent child list, freezes its lockstep pointer, forces the
following sibling to be paired by alternate, and the instance scan
(which only matches the paired previous fiber) no longer finds the
existing instance, since instances track the current fiber. The subtree
below is then walked without its instance, which cascades into spurious
unmount and remount work and surfaces at the assertion in the filtered
same-child-set branch.

We're also avoiding creation of new backend instances in those
scenarios.

This change bounds the previous set of the content pass by the previous
fallback fragment via a new `prevLastChild` parameter, so the flag
disappears.

Closes react#37280

## How did you test this change?

- cherry-picked test from react#37280

---------

Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
97.6% of the value sets tracked per identifier in mutation / aliasing
inference hold exactly one element, but each was a `FxHashSet`, meaning
each was a heap allocation.
Because the inference code retains a full state in each basic block,
these single-element hashsets were a major contributor to peak memory
allocation.

This replaces them with a small inline set inspired by smolvec /
tinyvec. Five values are stored inline, and any more spill to the heap.
This was only needed in **0.02%** of sets in my data corpus.

This also makes iteration order match the TS implementation. TS uses
`Set` and iterates in insertion order; the Fx set iterated in hash
order. This brings the two behaviors in line.

| Benchmark | Peak allocation | Allocation count | Wall time |

|------------------|-----------------------------|------------------|-----------|
| legacy/image.tsx | 33.40 -> 28.07 MiB (-16.0%) | -66.7% | -28.8% |
| next-client | 33.40 -> 28.07 (-16.0%) | -42.0% | -15.7% |
| devtools | 16.29 -> 14.25 (-12.5%) | -19.9% | -7.3% |
| fixtures | 9.41 -> 7.90 (-16.0%) | -7.3% | -3.3% |
| next-examples | 4.85 -> 4.85 ( 0.0%) | -4.8% | -1.6% |
…ning (react#37376)

tl;dr reduces memory churn by 7.8%, allocations by 4.1%, wall time by
~4%

## Summary

When compiled functions are written back to the AST,
`apply_compiled_functions` took a reference to a slice, and deep-cloned
each compiled body out of it. In theTS version this step assigns
references through Babel paths, which are essentially free. The Rust
port translated that as `.clone()` for safety, deep-copying the entire
codegen output for every compiled function.

Nothing needs these bodies after they are inserted, and the caller
already owns the vector. So this takes `compiled_fns` by value and moves
the data into the AST instead:

* `ReplaceFnVisitor` holds an `Option<CodegenFunction>` and moves it to
whatever arm matches
* Outlined function declarations move their id+params+body out of
`codegen_fn.outlined` rather than cloning them
* `needs_memo_import` is computed before the loop that consumes the
vector. Only the computation moved; the block that registers the import
stays where it was, so ordering and identifier numbering don't change.

This doesn't fully eliminate clones, just ones where it's easy to do a
move instead.

## How did you test this change?

All compiler fixtures pass with byte-identical output
Sizebot compared the pull request head build against the build of
`pull_request.base.sha`, which is the tip of the base branch at event
time, not the commit the pull request diverged from. The field's
semantics are undocumented in GitHub's API schema (the OpenAPI
description types it as a bare string); the observed behavior and the
compare API's `merge_base_commit` confirm the difference.

The difference between `pull_request.base.sha` and the merge base is
confirmed with an example in react#37356

The sizebot job now resolves the merge-base through the compare API and
downloads the base build for that commit instead, so the report only
ever contains the pull request's own changes. The job gains `contents:
read` for the compare call.

When no base build can be downloaded for the merge-base, for example
because its artifacts aged out of the retention window or its run
failed, the sizebot job records a `base-build-not-found` result instead
of failing immediately. `render-comment.js` on the default branch
renders that as a warning comment naming the base commit and writes the
`sizebot-problem.txt` marker, so the comment workflow fails its check
after posting the warning, the same pattern already used for build
configuration drift. The sizebot job itself intentionally stays green: a
failed run would make the renderer discard the results and mask the
warning with a generic "did not complete" message.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
## Summary

`git show --format=%h` is not stable: abbreviation length depends on
`core.abbrev` and how unique the prefix is in that clone. Two rebuilds
of the same commit can therefore embed different strings in
`DEVTOOLS_VERSION` and the extension manifest.

Always take the full hash (`%H`) and slice it to 10 characters so the
value is the same everywhere.

This also removes the `build/COMMIT_SHA` fallback used when Mozilla
rebuilds from a git archive (no `.git`). That path used a different
length (7) and a different source, so it could not match a git checkout
of the same commit. Firefox source review should rebuild from a checkout
of the commit in react#37307, not from the tarball alone.

Stack: this PR → react#37306react#37307.

## How did you test this change?

Build-script only. `getGitCommit()` now returns `HEAD` sliced to 10
chars, independent of `core.abbrev`.

Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
## Summary

The extension build writes `new Date().toLocaleDateString()` into
Chrome/Edge `version_name` and into every browser's manifest
description. That string changes with the calendar, timezone, and
locale, so a Firefox AMO rebuild on another day cannot match the
uploaded zip.

Stop stamping dates. `version_name` stays the value from the source
manifest (still updated by `prepare-release.js` on version bumps). The
description still records the commit from react#37305.

Depends on react#37305. Next: react#37307.

## How did you test this change?

Build-script only. After this, `manifest.json` description is `Created
from revision <commit>.` and Chrome/Edge `version_name` is the committed
version string.

---------

Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
## Summary

`build-and-test.js` could feed three different commits into one release:
`git archive main` (not `HEAD`), an interactively chosen React CI build,
and `HEAD` saved as metadata. Firefox source review then could not
reproduce the zip.

Require a clean tree, resolve `HEAD` once, and use that hash for the
source archive, the experimental React download, and the metadata
printed for AMO. Drop the prompt that let those diverge.

Depends on react#37305 and react#37306.

## How did you test this change?

Build-script only. The release helper now errors on a dirty tree and
threads a single `git rev-parse HEAD` into archive, download, and
metadata.

Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.

The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.

The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.

This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.

The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.

**Alternatives Considered**

- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.

Fixes react#37343
Closes react#37358
Closes react#37359

Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
…eact#37491)

The warning was previously only enabled for experimental builds
(`react@experimental`). This enables the warning for `react@canary` as
well.

Keep in mind that conditional `use()` is generally supported. This
warning only triggers if the condition is based on `promise.status` (or
`promise.value`). Let `use()` handle that status. React will not suspend
if the `promise.status` is already `'fulfilled'`.

More information can be found in the [`use()` docs under "Don’t skip
calling use based on whether a Promise is already
settled."](https://react.dev/reference/react/use#conditional-use).

We've tested this at Vercel on the latest version of SWR (which
previously had conditional `use()` calls) and found no false-positive
warnings or excessive warnings.
Apply the render's script nonce to import maps emitted through the
`importMap` server rendering option. This keeps configured import maps
compatible with nonce-based Content Security Policies and uses the same
escaped nonce value as other render-managed scripts.
Propagate errors from block lowering instead of continuing with
incomplete HIR.

This prevents the Rust compiler from emitting partial output when it
encounters unsupported syntax, including nested TypeScript `this`
parameters.

This landed earlier as react#37232, which exposed an existing FBT diagnostic
ordering issue. Resolve local FBT bindings before checking whether an
`<fbt>` tag comes from a module import, so the compiler reports the
earlier, more useful Todo instead of a later invariant.

Run from the repository root:

- `yarn --cwd compiler workspace babel-plugin-react-compiler-rust test`
- `cargo test --manifest-path compiler/Cargo.toml -p
react_compiler_lowering`
Add myself to maintainers
…nt (react#37496)

## Summary
- `react_get_component_by_dom_element` returns the host node for a DOM
element, which never has hooks.
- Stop advertising `hooks?` in the tool description so agents do not
request a field that is never returned.

## Test plan
- [ ] `yarn test --build --project=devtools -r=experimental
DevToolsCdtMcp`
…#37497)

## Summary
- Upgrade the cdt-mcp e2e dependency from chrome-devtools-mcp 1.3.0 to
1.8.0.
- Pass `pageId` to page-scoped CLI tools, which 1.8.0 requires.
- Use a hex `sessionId` (`crypto.randomUUID()`). 1.8.0 rejects ids that
are not `/[a-fA-F0-9-]+/`.

Stacked on react#37496

## Test plan
- [ ] `yarn --cwd packages/react-devtools-cdt-mcp test:e2e`
…37524)

Preserve the RefValue source location when joining mixed ref types so
validation errors point to the original ref access.

Before this change it used to show the incorrect location when using the
rust compiler.

```rust
  2 |   const ref = useRef(null);
  3 |   const x = cond ? ref : ref.current;
> 4 |   return <Foo value={x} />;
    |                      ^ Cannot access ref value during render
  5 | }
  6 |
```
…7503)

## Summary
- Rewrite the package README so npm consumers can tell this is a
page-side library, not an MCP server.
- Document the chrome-devtools-mcp 1.3.0+ requirement, the experimental
third-party flag, and the corrected DOM-tool output.

Stacked on react#37497

## Test plan
- [ ] Read the README as a first-time installer and confirm install,
import order, and MCP config are clear.
- [ ] Confirm `react_get_component_by_dom_element` no longer documents
`hooks`.
…eact#37542)

A row that is still parsing can hand its partially built value to
references that were registered on it during that parse.
`initializeModelChunk` fulfilled every such listener, on the assumption
that all of them are cyclic references back into the parsing row. Only
some are. A listener from a nested parse that is not part of a cycle
belongs to a handler that does not wait on the parsing row, so
fulfilling it early completes that handler with an object that still has
references outstanding.

When that handler owns an element, `initializeElement` runs on
incomplete props. In DEV the props are frozen, so the write that arrives
later throws `Cannot assign to read only property`, and
`rejectReference` escalates the error into the rows that wait on the
element, up to the root. Only the debug tree can produce this shape. The
RSC stream writes element props inline, while the debug channel outlines
a props object that an element shares with its own componentInfo into a
separate row, and that row can still wait on a client module.

`resolveBlockedCycle` already tells a cyclic reference from any other,
but it returned `null` for mid-parse listeners because `handler.chunk`
was assigned after the drain loop. This change assigns it before the
loop and classifies each listener. A reference whose handler is
transitively waiting on the parsing row is a genuine cycle and receives
the value now, because neither side can complete before the other. Every
other listener is queued back on the parsing row and fulfilled when that
row completes, like any reference into a blocked row.

A row whose own parse fails used to hand the partial value to its
mid-parse listeners before it threw. It now errors through
`triggerErrorOnChunk`, which rejects them the same way a reference into
any other errored row is rejected. The `if (handler.errored) throw`
after the loop is removed. It also caught a rejection during the loop,
which now reaches `triggerErrorOnChunk` on its own because
`handler.chunk` is set.

One behaviour changes beyond the reported bug. When
`initializeDebugChunk` errors a chunk before `parseModel` runs, the old
code set `INITIALIZED` over that status if the model had no pending
references, and left it `ERRORED` otherwise. The chunk now stays
`ERRORED` in both cases. That is what the `triggerErrorOnChunk` call in
`initializeDebugChunk` intends, and the TODO above the `parseModel` call
already notes that the chunk can be `ERRORED` there.

PR react#37398 deferred `Object.freeze(element.props)` until the outstanding
references have resolved. That removes the exception but not the cause:
the element is still initialized on an incomplete object and is visible
through `_debugInfo` with a `null` placeholder until the late write
lands. With the early release fixed, the freeze needs no change.

**Alternatives Considered**

- Deferring every listener whose handler is not the parsing row's own
deadlocks `foo ↔ bar` in `can deduped outlined references inside
promises`. One side of a genuine cycle has to accept the partial object.
- Holding an element back while its props row is `BLOCKED` breaks
`should handle deduped props of re-used elements in fragments`, where
the row is blocked on an unrelated module and the props object itself is
complete.
- A per-object count of pending writes plus a reverse `dependents` edge
works, but adds a second dependency graph next to `deps` and
special-cases elements.

Fixes react#37361
Closes react#37398
Before things like 1e21 | 0 would return -1 instead of -559939584 like
js expects. This should make this match in all cases.
## Summary
- Bump `react-devtools-cdt-mcp` from the placeholder `0.0.0` to `0.1.0`.
- Describe the package as a browser library that registers React tools
with chrome-devtools-mcp.

Stacked on react#37503

## Test plan
- [ ] Confirm `packages/react-devtools-cdt-mcp/package.json` is `0.1.0`
before publishing.
- [ ] Dry-run the publish workflow after this PR.
…-cdt-mcp (react#37498)

## Summary
- Add a manual `workflow_dispatch` workflow that publishes only
`react-devtools-cdt-mcp`.
- Match the runtime release security model: empty default permissions,
checkout of `github.sha` only, protected `npm` environment, Node 24, and
OIDC trusted publishing (no `NPM_TOKEN`).

Stacked on react#37500

## Test plan
- [ ] Open the workflow in GitHub Actions and confirm it is
`workflow_dispatch` only.
- [ ] Dry-run dispatch after the version bump lands, once npm trusted
publishing is configured for this package.
## Summary
- Bump React DevTools from 7.0.1 to 8.0.0 (packages + extension
manifests only; no publish).
- `scripts/devtools/prepare-release.js` only supports minor/patch, so
this major bump is manual.
- Changelog is curated from DevTools commits since 7.0.1 (features vs
bugfixes; internal/test-only changes omitted).

## Test plan
- [ ] Confirm published package versions would be 8.0.0 for
`react-devtools`, `react-devtools-core`, and `react-devtools-inline`
- [ ] Confirm Chrome/Edge/Firefox extension manifests show 8.0.0
- [ ] Review `packages/react-devtools/CHANGELOG.md` 8.0.0 section for
accuracy before undrafting
…eact#37539)

## Summary

`get_identifier_name_with_loc` reads an identifier's name out of the
source when SSA has dropped it. `SourceLocation.index` is a Babel
position and counts UTF-16 code units, but the fallback used it to slice
a Rust `&str`, which indexes UTF-8 bytes:

```rust
let slice = &code[start_idx..end_idx];
```

These agree only while the source is ASCII. After any non-ASCII
character, later offsets are short by the extra bytes, so the slice
reads the wrong span, or panics when it lands inside a character:

```
panicked at crates/react_compiler_validation/src/validate_no_set_state_in_effects.rs:168:30:
start byte index 637 is not a char boundary; it is inside 'う' (bytes 636..639 of string)
```

The panic aborts validation for the whole file, so every diagnostic in
it is lost.

`validate_no_derived_computations_in_effects.rs` already converts
correctly in its own copy of this function. Both were added in react#36173
and only one got the UTF-16 handling, so this ports that logic over.

Found through Biome, which embeds these crates for its
`useReactCompiler` rule.

## How did you test this change?

Added `repro-setState-in-effect-non-ascii-source.ts`, which panics
before this change and compiles cleanly after:

```
bash scripts/test-rust-port.sh ValidateNoSetStateInEffects \
  packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-setState-in-effect-non-ascii-source.ts
```

The non-ASCII comments in it are test input, not documentation; removing
them realigns the offsets and the crash disappears.

Everything `compiler_rust.yml` runs is green on macOS arm64, including
`scripts/test-rust-port.sh` (1811 passed) and `yarn snap --rust` (1812
passed).
Adds the `19.3.0` changelog entry, covering everything that has landed
on `main` since `v19.2.0`.
## Summary

`enableFragmentRefs` is enabled in every channel, so this inlines the
enabled branch and removes the flag from `ReactFeatureFlags` and all of
its forks.

Most of the diff is mechanical, but a few spots needed care:

- Several `case Fragment:` blocks in `ReactFiberCommitWork` had a `//
Fallthrough` that was only reachable with the flag off. Where a
preceding case falls *into* `Fragment` (the `ViewTransitionComponent`
cases), I verified the resulting behavior is unchanged for every
remaining flag combination.
- `commitAttachRef` in `ReactFiberCommitEffects` becomes a plain `case
Fragment: { ... break; }` instead of a conditional fallthrough into
`default`.
- The `React.Fragment` invalid-prop warning no longer has two variants;
it always mentions `key`, `ref`, and `children`.

The three related flags — `enableFragmentRefsScrollIntoView`,
`enableFragmentRefsInstanceHandles`, and `enableFragmentRefsTextNodes` —
are *not* on everywhere yet and are left in place.

In tests, `enableFragmentRefs` was stripped from 101 `@gate` pragmas.
Combined gates such as `@gate enableFragmentRefs &&
enableFragmentRefsTextNodes` were reduced rather than removed. One test
asserted the absence of a warning under the flag, so it was renamed from
`warns for fragments with refs` to `does not warn for fragments with
refs`.

## How did you test this change?

`yarn lint`, `yarn prettier-check`, and `yarn flow` for `dom-node`,
`dom-browser`, and `fabric` all pass.

Full test suite run across `experimental`, `stable`, `www-modern`,
`www-classic`, and `xplat`, with both `--variant` settings, plus
`--persistent`. The remaining failures (Fizz / Flight / FrameScheduling
/ ClassEquivalence) are pre-existing: I diffed the individual failing
test names against the base commit and they are identical.
Align Rspack with react-server-dom-webpack after merging React main.
Pass AbortSignal to processReply and use attachAbortSignal for render and
prerender requests so abort listeners follow the request lifetime.

Add the missing yarn.lock entry for the existing resolve.exports dependency.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.