Sync Rspack with React main and webpack cancellation handling - #2
Open
nikhilsnayak wants to merge 66 commits into
Open
nikhilsnayak wants to merge 66 commits into
nikhilsnayak wants to merge 66 commits into
Conversation
…#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
…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`
…rs (react#37232)" (react#37363) This reverts commit 35e64cf.
…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>
…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#37306 → react#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
…react#37549) Required for trusted publishing, same as react#36752
…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).
## Summary ReactNativeFeatureFlags and fabricUIManager were introduced in `react-native/react-private-interface` in react/react-native#57940 and react/react-native#58398: https://github.com/react/react-native/blob/73a76ddced2088d925809432fd6b0503f239dbfe/packages/react-native/src/react-private-interface.js#L74 https://github.com/react/react-native/blob/73a76ddced2088d925809432fd6b0503f239dbfe/packages/react-native/src/react-private-interface.js#L87 This migrates the use of globals for those bindings to use the exported values instead. ## How did you test this change? Existing tests and Flow.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merge React main (
00f48cecf) intoreact-server-dom-rspackand align the Rspack adapters with webpack's cancellation handling: pass the signal toprocessReplyand useattachAbortSignalfor render/prerender requests. Add the missing lock entry for the existingresolve.exportsdependency.How did you test this change?
ReactFlightClient.js, also reproduced with the webpack baseline. An upstream webpack composite-signal test fails under the local Node 24/Jest AbortController setup.