Added behind a new experimental flag, `enableFlightObjectReferences`.
Extends Server References so that a reference can point to an object,
not just a function. An object is registered with a new API,
`registerServerObjectReference`, which tags it with its module id the
same way `registerServerReference` tags a Server Function.
The client receives an opaque handle: reading a property or calling it
throws, similar to how Temporary References work inside Server
Functions. The only thing the client can do with the handle is pass it
back to the server via a Server Function, where it resolves through the
server manifest.
The motivating use case is letting a framework pass request-scoped
values by reference, so it can omit them from the request body. For
example, a framework could model `searchParams` as a module export
whose value resolves from the current request. This avoids encoding the
search params twice (they already appear in the request URL) but it
also makes the responses more cacheable: if the search params don't
appear elsewhere in the body, then a response cache can omit them from
its cache key.
This initial PR only exposes the new API in the Turbopack bindings. We
will port it to the other bundler configs if the experiment advances.
Pulls the chunk-state machinery out of loadServerReference into two
helpers: resolveServerReferenceChunk (settle a blocked chunk to its
resolved value and wake any listeners) and readServerReference (return a
resolved chunk's value, or wait on a blocked one). loadServerReference
keeps its own local fulfill that computes the value and reports failures
via triggerErrorOnChunk. A follow-up reuses these for references that
point at objects.
Behavior-preserving for the function reference path; the only change is
that its resolution error handling now routes through triggerErrorOnChunk
like the rest of the decoder.
## Summary
`FragmentInstance.compareDocumentPosition()` returns
`DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC` instead of `CONTAINS |
PRECEDING` for the container passed to `createRoot()`, and for any
element between that container and `document.body`:
```js
const container = document.getElementById('root');
createRoot(container).render(<div><Fragment ref={ref}><div /></Fragment></div>);
ref.current.compareDocumentPosition(document.body); // contains
ref.current.compareDocumentPosition(container); // implementationSpecific
```
The `CONTAINS` fiber validation enumerates the nodes that have no fiber
as `document`, `documentElement` and `body`. That list is never
complete: the root container has no fiber either, and neither does any
non-React element above it, so both fall through and are reported as
implementation specific.
This checks that `otherNode` contains the root container instead, which
covers all of those uniformly.
Adjacent to #37579, which fixed a `null` dereference in the same branch.
## Summary
Fixes#22718.
**Root cause:** `toggle` and `beforetoggle` (fired by `<details>`,
`<dialog>`, and the Popover API) don't bubble natively in the DOM, and
they were already listed in `nonDelegatedEvents` (native listener
attached directly to the target). However, `SimpleEventPlugin`'s
`accumulateTargetOnly` check — which controls whether React accumulates
listeners for just the target vs. the whole ancestor chain — only
special-cased `scroll`/`scrollend`. So `toggle`/`beforetoggle` still had
their listeners accumulated up the fiber tree, meaning ancestor
`onToggle`/`onBeforeToggle` handlers could fire even though the native
event never reached them.
**Fix:** Extend the existing `accumulateTargetOnly` condition in
`packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js` to
also cover `toggle` and `beforetoggle`, aligning them with the
scroll/scrollend non-bubbling precedent. No other files touched.
**Tests:** Moved the existing `onToggle`/`onBeforeToggle` test cases
(plain `<details>`, Popover API, Dialog API) in
`ReactDOMEventPropagation-test.js` from the "non-bubbling events that
bubble in React" describe block (`testEmulatedBubblingEvent`) into
"non-bubbling events that do not bubble in React"
(`testNonBubblingEvent`), matching how `onScroll`/`onScrollEnd` are
tested. Also added a small `...eventConfig.targetProps` spread to the
`testNonBubblingEvent*` helpers so `targetProps` (e.g. `popover: 'any'`)
reach the fixture, mirroring the `testEmulatedBubblingEvent*` helpers.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Summary
V8 prefixes async call sites with `async ` when it prints a stack, like
this:
```
Error: boom
at inner (/tmp/asy.js:1:44)
at async outerName (/tmp/asy.js:2:30)
```
`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:
```js
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
```
`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.
I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.
The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. #37130 is open on that file
too, but it does not touch these lines.
## How did you test this change?
I first confirmed the format V8 actually emits, rather than assuming it:
```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
at inner ([eval]:1:52)
at async outerName ([eval]:2:33)
```
Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:
```
● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name
- Expected - 1
+ Received + 1
Array [
- "Comments",
+ " Comments",
"https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
5,
9236,
]
```
With the one-character fix applied:
```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests: 63 passed, 63 total
```
I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.
For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).
`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.
AI tools used
## Summary
AI Disclosure: This was assisted using Claude Code, Fable 5.1. This PR
description was fully written by me, manually, and the code was reviewed
and tested by me manually.
This improves the accuracy of the animation-related event props
supported by React.
This adds `onAnimationCancel` alongside the existing `onAnimationStart`,
`onAnimationIteration`, and `onAnimationEnd` props.
Adds `onAnimationCancel` to React DOM, completing the animation event
family alongside `onAnimationStart`, `onAnimationIteration`, and
`onAnimationEnd`.
[`animationcancel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationcancel_event)
is a CSS Animations Level 1 event fired when an animation stops before
it has completed.
This changeset is based on the similar changes in #27345 for the
transition events.
Note that MDN's browser compat data [currently
claims](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationcancel_event)
that this is not supported in Chrome or Edge (at least not as
`onanimationcancel`), but this change will work in Chrome 83+ due to
React passing things through `addEventListener` anyway, and also the MDN
docs are [outdated about
this](https://github.com/mdn/browser-compat-data/issues/29376), Chrome
has started supporting `onanimationcancel` directly [since earlier this
year](https://issues.chromium.org/issues/41404325).
Also note that the animation and transition events on DOMEventName can
probably be made to no longer do this vendor prefixing dance? I didn't
do that in this PR and it should be done separately, but they've not
been prefixed in a long time now and the comment rationalizing the
current code mentions Android 4.x, which was last released over a decade
ago now. So it seems like something that has just never been cleaned up.
## How did you test this change?
- Added tests to match the sibling properties' tests.
- Ran `yarn test`, `yarn lint`, and `yarn prettier` and all passed.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
## Summary
Fixes#37578.
`FragmentInstance.compareDocumentPosition(document)` threw:
```
TypeError: Cannot read properties of null (reading 'documentElement')
```
because `Document.ownerDocument` is `null`, and the `CONTAINS`
fiber-validation fallback read `ownerDocument.documentElement`
unconditionally. `document.body` / `document.documentElement` already
worked.
This treats a `Document` node as its own owner document, matching
`Node.compareDocumentPosition(document)`.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
> [!NOTE]
> Stacked on #37573. That PR's commit shows up in the diff here until it
lands; only the second commit is part of this change.
## Summary
Inlines the enabled branch of `enableFragmentRefsScrollIntoView` and
removes the flag from `ReactFeatureFlags` and all of its forks.
This is a DOM-only change. The flag's only consumer was
`ReactFiberConfigDOM`, where it guarded the definition of
`FragmentInstance.prototype.scrollIntoView`, so the code change is a
plain unwrap and dedent of that one block with no logic edits.
The flag was declared `false` in the native forks, but those
declarations were inert: Fabric never defined `scrollIntoView` on
`FragmentInstance` at all, so removing the flag does not change any
native behavior.
The one combined gate, `@gate enableFragmentRefsTextNodes &&
enableFragmentRefsScrollIntoView`, was reduced to `@gate
enableFragmentRefsTextNodes` rather than dropped.
## 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 parent commit and they are identical.
## Summary
- support prefix and postfix update expressions on captured variables in
both TypeScript and Rust compiler backends
- represent updates with explicit `PrefixUpdateLocal`,
`PrefixUpdateContext`, `PostfixUpdateLocal`, and `PostfixUpdateContext`
HIR variants
- model captured updates as mutations so SSA, effect inference,
dead-code elimination, and post-render validation preserve their
semantics
- convert the existing TODO fixtures to passing coverage and add the
Airwave-shaped map regression from T286959188
- correct Rust workspace manifest paths in the rust-port documentation
## How did you test this change?
- `yarn prettier`
- `yarn linc`
- `yarn flow dom-node`
- `yarn test --silent --no-watchman React-hooks-arity`
- `yarn test-www --silent --no-watchman useMemoCache`
- `yarn test-www --variant=false --silent --no-watchman useMemoCache`
- `node_modules/.bin/tsc --noEmit -p
compiler/packages/babel-plugin-react-compiler/tsconfig.json`
- `cargo +1.93.1 fmt --manifest-path compiler/Cargo.toml --all --
--check`
- `cargo +1.93.1 check --manifest-path compiler/Cargo.toml -p
react_compiler -j 1`
- targeted Cargo tests for lowering, SSA, inference, optimization, and
validation
- TypeScript and Rust snapshot runs for `*update-expression*` (11 tests
each)
## 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.
## 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 #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).
## 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
## 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 https://github.com/react/react/pull/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-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 https://github.com/react/react/pull/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.
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 #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#37361Closes#37398
## 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 https://github.com/react/react/pull/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`.
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 |
```
## 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 https://github.com/react/react/pull/37496
## Test plan
- [ ] `yarn --cwd packages/react-devtools-cdt-mcp test:e2e`
## 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`
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 #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`
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.
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.
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 #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #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 #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #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. #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#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.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 #37305 and #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>
## 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 #37305.
Depends on #37305. Next: #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
`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 #37307, not from the tarball alone.
Stack: this PR → #37306 → #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>
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 https://github.com/react/react/pull/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>
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
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% |
## Summary
Alternative to #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#37280
## How did you test this change?
- cherry-picked test from #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>
## Summary
- configure the React Compiler Rust crates as a Cargo workspace with
shared package metadata and versioned internal dependencies
- add workflows to open a signed version-bump pull request and publish
the workspace through crates.io trusted publishing
- add release scripts and contributor documentation for versioning,
validation, and publishing
## Test plan
- Run `cargo check --locked --workspace` from `compiler/`.
- Run **(Compiler) Publish Rust Crates** from the Actions tab with **Dry
run** enabled; confirm every workspace crate packages successfully
without publishing.
- Run **(Compiler) Update Rust Crate Version** with a test version;
confirm it verifies one shared version and opens a signed version-bump
pull request containing the updated workspace manifest and lockfile.
Listeners registered with `options.signal` are supposed to be removed
when the signal is aborted.
Since `FragmentInstance` does not clean up its tracked listeners on
abort, previously removed listeners cannot be re-attached.
Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.
If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.
Closes https://github.com/react/react/pull/37452/
Closes https://github.com/react/react/pull/37302
Fixes https://github.com/react/react/issues/37451
Bumps Jest to latest 30.x
The `resolutions` pin that kept jsdom at 22.1.0 is removed, so the test
environment now runs the jsdom version that jest-environment-jsdom
declares (26.1.0 on Jest 30).
The matcher aliases that Jest 30 deleted are replaced with their
canonical forms across the test suites: `toBeCalled`, `toBeCalledTimes`,
`toBeCalledWith`, and `lastCalledWith` become the corresponding
`toHaveBeenCalled*` matchers, and `toThrowError` becomes `toThrow`. The
custom `toThrow` matcher override is removed in the PR below this one.
Jest 30 activates the `node` export condition for CommonJS requires in
every test environment, so in the jsdom-based Flight suites
`react-server-dom-webpack/client` now resolves to the Node build (which
requires an options argument) instead of the browser build. Those suites
now map the client entry to `client.browser` explicitly, matching the
existing mocks for the `server` and `static` entries, and the Turbopack
Node test uses `jest.requireActual` because `client` and `client.node`
now resolve to the same file, which otherwise made the mock factory
recurse. For the same reason `react-dom/static` resolves to the
lazily-initialized Node entry in source mode, so its version-mismatch
test is gated to build mode like the other server-entry tests.
The obsolete `prettierPath` override is dropped from the base Jest
config because Jest 30 works with Prettier 3 for inline snapshots, which
also fixes `yarn test -u` crashing on the repo's Prettier 3-only hermes
plugin, and the now unused `prettier-2` alias dependency is removed with
it. Snapshot files are regenerated for Jest 30's updated snapshot header
and formatting. One test now passes a number instead of a string to
`jest.advanceTimersByTime`, which fake-timers v13 no longer coerces.
---------
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
`ToggleEvent` carries a `source` property pointing at the control that
opened or closed a popover, but `ToggleEventInterface` only lists
`newState` and `oldState`, so the synthetic event never copies it. An
`onToggle` handler reads `event.source` as `undefined` even when the
native event has it.
Adding `source` to the interface copies it off the native event the same
way `newState` and `oldState` are copied.
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>
`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>
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>
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>
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>
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>