Builds on #37049 by validating Store operation invariants before
mutation. Missing nodes, invalid element types, inconsistent
parent-child relationships, and invalid reorder operations now emit and
throw explicit errors instead of silently continuing with corrupted
state.
Adds a canonical-render regression test for invalid child removal.
Builds on #37048 by replacing `any`-based Bridge and Wall boundaries
with typed `mixed` values and explicit runtime validation. Invalid
messages and post-shutdown operations now throw, while shutdown reliably
flushes queued messages even if cleanup fails.
Strengthens the DevTools Bridge and Wall contracts:
- Models event dictionaries as event-to-payload maps, using `void` for
events without payloads.
- Types `send(event, payload?)` directly, eliminating runtime
payload-arity handling.
- Replaces broad `any` transport types with `mixed` and boundary
validation.
- Throws on invalid lifecycle usage instead of warning or silently
returning.
- Ensures shutdown flushes queued messages even when Wall cleanup fails.
- Updates Wall implementations and adds Bridge lifecycle coverage.
Ensures the standalone DevTools Bridge fully shuts down when its
WebSocket closes, with re-entrancy protection. Adds tests confirming
event-only shutdown leaves the Bridge active while socket closure shuts
it down.
Tightens `EventEmitter` listener types and fixes error handling so the
first thrown value is preserved while subsequent errors are reported
instead of swallowed. Adds regression coverage for listener failures.
Turbopack has introduced a new format for chunks (experimental):
```typescript
type ChunkUrlOrMerged = ChunkUrl | [ChunkUrl, ChunkPath[], number[]]
```
This will allow us to dynamically choose to merge / unmerge chunks based
on already loaded chunks. See
https://github.com/vercel/next.js/pull/95261 for more context behind
these feature.
However, we noticed that this breaks `prepareDestinationWithChunks()` on
server-side renders. The prepared chunk script tags were a stringified
version of this array:
<img width="1758" height="474" alt="Screenshot 2026-07-22 at 9 37 06 am"
src="https://github.com/user-attachments/assets/fa273b44-5942-4037-b955-4338472242fe"
/>
Instead, we should prepare the merged chunk as these SSR-d pages would
not have any loaded chunks tracked in memory (that happens on the
client).
This PR brings back this optimisation and also would remove the
console-logged errors associated with them.
---------
Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>
Fixes#27670.
When an Activity subtree is hidden, its passive effects are
disconnected, which unsubscribes from the store. On reveal, the effects
are reconnected by replaying the fiber's effect list without a render,
but if `updateStoreInstance` was not in the effect list then the
component would be left stale. This happened both when a layout effect
mutated the store during the reveal commit (after the subtree rendered
but before it resubscribed) and when the store changed while hidden and
the component bailed out of rendering during the reveal.
We now push the updateStoreInstance effect unconditionally but tag it
with HookHasEffect only under the same conditions as before, so regular
commits skip it when nothing changed but reconnection always triggers it
so it can trigger a rerender if appropriate.
e.g. `render(<Fragment ref={...}>{children}</Fragment>)` where
`children` contain no Host Components.
For `Document` we just noop because some part of the document is always
in view.
For `ShadowRoot` we use its `host`.
Any other `DocumentFragment` without a `host` issues a warning because
there's nothing to scroll. This matches the existing warning when you
call `scrollIntoView(false)` on an empty Fragment without siblings.
Notably this also applies to `<html><body /></html>` in `children`
because we skip `HostSingleton` at the moment.
---------
Co-authored-by: Hanzo Dev <[email protected]>
Adds SSR support for nested parentEnter/parentExit View Transitions.
Fizz now emits vt-parent-enter/vt-parent-exit annotations during
streaming, and the client picks them up on hydration, so nested
enter/exit animations work for Suspense reveals.
When a Fragment has no children, React would consider scrolling to
siblings first and then to parents. However, React only considered
`HostComponent` for the siblings.
Since we already have a heuristic for scrolling to `HostText`, we can
reuse that same heuristic.
---------
Co-authored-by: Hanzo Dev <[email protected]>
Ran into this test failure as part of
https://github.com/react/react/pull/36917 - it seems that the added code
was just enough to increase stack size and fail the deep tree recovery
test in CI. Looking into that, there appears to be a gap here with
retries, including a TODO test case for the scenario.
Fizz recovers from stack overflows in extremely deep trees by catching
the first overflow in the `renderNode` trampoline and spawning a
continuation task. That continuation is retried via `retryRenderTask →
retryNode`, which has no trampoline above it. So if the remaining tree
still doesn't fit in one fresh stack, the overflow was treated as a
fatal error instead of recovering again.
This fix re-schedules the task when a retried render overflows but
`task.node` advanced (proving forward progress was made). If this is a
real in-component overflow, `task.node` doesn't advance and we still
fail.
The existing test used `n={1000}`, which only required one recovery
round and didn't catch this gap in source mode. It's updated to
`n={1200}`, which reliably requires multiple recovery rounds.
We should provide a way for consumers to handle the installation on
their own, so the index entrypoint will just export the APIs and won't
have any side-effects.
The `/register` entrypoint (via `import
react-devtools-cdt-mcp/register`), will subscribe an event listener and
install the tools.
Adds support for the scenario, when the package is imported multiple
times for the same target.
This will return the same (referentially) facade object with tools, and
will only clean up if all invocations were cleaned up.
This add an additional validation that the package is imported in a
browser-like environment. If not, we should be explicit with the user
and throw an error.
## Summary
Microtasks are enabled by default in RN already, so there's no need to
gate their use in Fabric atm.
## How did you test this change?
Existing tests. Tested e2e at Meta.
We used to avoid updating .defaultValue until blur on number inputs
because of a Chrome bug, but the relevant Chromium behavior was fixed in
2020 in M85 https://issues.chromium.org/issues/40124871 and likewise in
WebKit https://bugs.webkit.org/show_bug.cgi?id=217156 so I think we can
probably just remove this special-cased logic.
We do still need some special logic for number inputs because it's the
only type where we let users specify a non-string value that has
multiple string representations and need to be careful not to clobber
the .value if it has a slightly different string than what you'd get
from casting a number to string.
Fixes#29862.
Tested with the number inputs fixture in current Chrome, Safari, and
Firefox.
Two bugs:
- Type changes to a lazy's inner component (e.g., adding a `memo`
comparison function) were missed by the refresh scan.
- If a SimpleMemoComponent needs to be remounted, previously
resolveRemountTypeForHotReloading would return the inner function
and we would incorrectly recreate the fiber as a FunctionComponent.
Instead, we can just use elementType consistently now that the
beginWork functions consistently call resolveFamily on the wrapped
components.
Editing the second argument of memo() previously never took effect
until something else remounted the tree, for two separate reasons:
- When adding a comparison function, we need to switch from
SimpleMemoComponent to MemoComponent so canPreserveStateBetween
should return false.
- MemoComponent was missing from the hot reload type resolution in
createWorkInProgress, so existing fibers kept reading .compare from
the old memo object forever.
New behavior:
- Adding or removing the comparison function remounts; we need to do
this when adding (SimpleMemoComponent doesn't support a comparison
function) so let's also do it when removing.
- Editing the comparison function implementation applies in place
with state preserved
The TODO in isCompatibleFamilyForHotReloading is removed as that
wasn't the right place to do this check.
If an edit changed the kind of the type (e.g. a plain function got
wrapped in memo()), the fiber could get the old kind's tag with the
new kind's type, usually throwing an error.
Previously resolveForwardRefForHotReloading was treated as a special
case. It's simpler to move this logic to updateForwardRef which
mirrors how updateMemoComponent and mountLazyComponent already are,
and it allows us to unify to a single resolveTypeForHotReloading which
we'll use in the next commit.
Since .type is no longer changing we need to set didReceiveUpdate
manually.
This behavior is already tested in "can update forwardRef render
function in isolation".
Same as #35227, but for `set`.
V8's `stack` setter silently noops when `this` is a Proxy
(nodejs/node#60862), so prodstack-stripping assignments like
`error.stack = 'Error: ' + message` in `resolveErrorProd` were dropped
and reads returned the real stack.
This caused `yarn test --prod` failures on Node 21+.
## Test Plan
`yarn test --prod` passes on both old and new Nodes.
Co-authored-by: Hanzo Dev <[email protected]>
Claude found this while running the Flight/Fizz bench.
---
While measuring memory in this fixture, the Flight server appeared to
leak ~140KB per render: `heapUsed` grew steadily across hundreds of
renders even with forced GC, and roughly 3x worse when a Flight client
consumed the stream in the same process. Fizz-only runs were flat, so it
looked like a real Flight leak.
On a closer look, the benchmark loop seems to be lying. React schedules
one `setImmediate` per request, but every render in this fixture
completes entirely in promises and `nextTick` callbacks, so a tight
benchmark loop never gives the event loop a chance to run immediates at
all. They quietly pile up, and each pending callback keeps its
already-finished request alive.
Heap snapshots show that every "leaked" request was held by a pending
immediate, and nothing else. Adding a single `await setImmediate` per
iteration makes memory completely flat over 300 renders, with or without
a same-process Flight client. A real server yields to the event loop on
every request, so this can't happen outside a synthetic loop.
The fix is to yield to the event loop between benchmark iterations,
outside the timed window, so latency numbers are unaffected. Each
variant now also reports how much heap it retains after the run settles.
That number was meaningless before — the pile-up made variants with
bigger per-request graphs look like they used more memory when they
didn't.
After the fix, a full `yarn bench:bare` run retains +0.3–0.6MB total per
variant after 1000 iterations (JIT/warmup noise), instead of drifting by
hundreds of MB.
---------
Co-authored-by: Hanzo Dev <[email protected]>
## Summary
Newer V8 versions (Node.js ≥ ~22) name the stack frames of static
methods on the `Promise` constructor `Promise.all`, `Promise.race`,
etc., where older versions named them `Function.all`, `Function.race`,
etc. The `isPromiseCreationInternal` and `isPromiseAwaitInternal`
allowlists in `ReactFlightServer` only matched the old spellings.
## How did you test this change?
- `yarn test --no-watchman ReactFlightAsyncDebugInfo` — 18/18 pass on
both Node 20.19.0 and Node 24.16.0 (previously, "can track async
information when awaited" failed on Node 24)
- `yarn test --silent --no-watchman packages/react-server/src/__tests__
packages/react-server-dom-webpack` — 243/243 pass on both Node versions,
default and `-r=experimental` channels
- `yarn test-www --silent --no-watchman
packages/react-server/src/__tests__` with `__VARIANT__` true and false —
pass
- `yarn flow dom-node`, `yarn prettier`, `yarn linc` — pass
Co-authored-by: Hanzo Dev <[email protected]>
## Summary
The compiler silently rewrote functions containing `using` and `await
using` declarations (explicit resource management). BuildHIR's
`VariableDeclaration` case only special-cases `var`; every other kind
falls through to `InstructionKind.Const`. A `using` declaration
therefore compiled as a plain memoized `const`, and the implicit dispose
call at scope exit was dropped from the compiled output. No diagnostic
was emitted. The Rust port mirrored the same bug in `build_hir.rs`.
The Rust port had a second, harder failure: its
`VariableDeclarationKind` enum had no `await using` variant, so a Babel
AST containing one failed serde deserialization at the NAPI boundary
("unknown variant `await using`") and threw for the whole file.
The fix records the same per-function Todo bail as the existing `var`
case, in both implementations, then continues lowering the declaration
as `const` so references do not break while the error unwinds (mirroring
how `var` continues as `let`). This matches the compiler's established
Todo-bail precedent for unsupported syntax: the function is skipped with
a logged `CompileError` diagnostic, `panicThreshold` is respected, and
sibling functions in the same file still compile. This deliberately
contrasts with oxc-project/oxc#24217, which handles the same syntax with
a silent per-function skip; nothing in this compiler is skipped without
a surfaced diagnostic.
Changes:
- TS: `BuildHIR.ts` records a Todo `CompilerErrorDetail` for `using` and
`await using` kinds, in the style of the existing `var` case.
- Rust: same Todo `record_error` in `build_hir.rs`. New `AwaitUsing`
variant (serde name `"await using"`) in `react_compiler_ast`, so `await
using` survives the NAPI boundary, covered by a serde round-trip unit
test.
- Test harness: enables the `explicitResourceManagement` parser plugin
in snap and in the e2e script's Babel baseline so fixtures can use the
syntax.
Fixtures (identical snapshots for the TS and Rust pipelines):
- `error.todo-using-declaration` and
`error.todo-await-using-declaration` show the Todo diagnostic with code
frames.
- `using-declaration-bailout-sibling-compiles` shows the bailing
function left untouched (disposal preserved) while a sibling component
in the same file is memoized.
The first commit snapshots the previous broken behavior (`using`
memoized as `const` with disposal dropped); the second commit lands the
fix and updates the snapshots.
## How did you test this change?
All results below are from the branch rebased onto current main.
TS implementation:
- `yarn snap`: 1809 tests, 1809 passed, 0 failed.
- `yarn workspace babel-plugin-react-compiler lint`: clean. `yarn
prettier-check` at the repo root: clean.
- Manually verified pre-fix output: a component with `using resource =
getResource(props.id)` compiled to a memoized `const` with no disposal.
Rust implementation:
- `cargo test --workspace`: 42 passed, 0 failed (includes the new serde
round-trip unit test for both `using` kinds).
- `bash scripts/test-babel-ast.sh`: passed.
- `bash scripts/test-rust-port.sh`: 1808 passed, 0 failed.
- `yarn snap --rust`: 1809 tests, 1809 passed, 0 failed, against the
same `.expect.md` snapshots as the TS run.
- `scripts/test-e2e.ts` (babel variant): the three using fixtures pass
code and events parity.
- Manually verified `await using` now deserializes across the NAPI
boundary and records the Todo instead of throwing.
<div><a
href="https://cursor.com/agents/bc-5fe4350d-5d42-4c0e-9001-429ed0b5e5b7"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/background-agent?bcId=bc-5fe4350d-5d42-4c0e-9001-429ed0b5e5b7"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
---------
Co-authored-by: Hanzo Dev <[email protected]>
Fixes#30659. I'm not confident in this yet but here's what Fable said
based on
https://github.com/react/react/pull/32214#issuecomment-2734436781:
Editing a component from a plain function to a memo or forwardRef
wrapper (or between wrapper kinds) crashed with "Component is not a
function", because canPreserveStateBetween only compared hook
signatures. The edit was classified as state-preserving, so
createWorkInProgress swapped the wrapper object in as the type of a
FunctionComponent fiber and renderWithHooks tried to call it.
Two fixes, both DEV-only:
- canPreserveStateBetween returns false when typeof or $$typeof differ.
A fiber's tag is derived from the kind of its type, so state can never
be preserved across a kind change; these edits must go to staleFamilies
(remount). Nested wrappers need no special handling because register()
creates a family per nesting level ($type/$render ids), and each level's
kind is compared against its own family.
- The reconciler now finds and remounts wrapper fibers whose outer kind
changed. scheduleFibersWithFamiliesRecursively previously resolved
families only through the inner implementation (type/type.render), but a
kind-changing edit is recorded only on the outer type's family, so edits
like memo -> function never reached the scanner and were silently
dropped. The scanner now also checks the elementType's family, and the
_debugNeedsRemount branch in beginWork rebuilds the fiber from
family.current (the latest registered type for the fiber's identity)
rather than the fiber's possibly-stale inner type. This also fixes stale
simple memo remounts recreating as plain functions, dropping the memo
wrapper.
Unlike the reverted #30660 (see #32214), this does not fabricate
families or guess type shapes: families are still created in exactly one
place, and the same ID always resolves to the same family.
Restores the tests from #30660 and adds coverage for changing the inner
type of a memo between function and forwardRef.
Co-authored-by: BIKI DAS <[email protected]>
Co-authored-by: dan <[email protected]>
## Summary
Fixes#36601
### Problem
In `lowerJsxElementName` (BuildHIR.ts), the condition `if
(tag.match(/^[A-Z]/))` only treats JSX tags starting with an **uppercase
letter** as component references. Tags starting with `_`, `$`, or any
other non-letter character fell through to the `else` branch and were
incorrectly classified as `BuiltinTag` (host/intrinsic elements).
This meant that `<_Bar />` or `<$Foo />` was treated like `<div />`,
causing the compiler to skip memoization of the component and
potentially producing incorrect output.
### Root Cause
JSX semantics (as implemented by Babel's JSX transform) are:
- Tag starts with **lowercase** letter → host/intrinsic element (string
tag)
- **Everything else** → component reference (in-scope identifier)
The original code only handled the first half of that rule ("starts with
uppercase → component") while ignoring identifiers like `_Bar` and
`$Foo`.
### Fix
Change:
```ts
if (tag.match(/^[A-Z]/)) {
```
To:
```ts
if (!tag.match(/^[a-z]/)) {
```
This correctly classifies any JSX identifier that does NOT start with a
lowercase letter as a component reference, matching JSX spec semantics.
### Test
Added fixture `jsx-underscore-prefix-component` that renders `<_Bar />`
and verifies the compiler correctly memoizes it as a component
reference.
## How did you test this change?
- Added new compiler fixture test: `jsx-underscore-prefix-component`
- Ran `yarn workspace babel-plugin-react-compiler lint` ✅
- Ran snap tests with the new fixture to generate expected output ✅
## Summary
#34493 added `@tanstack/react-virtual`'s `useVirtualizer` to the known
incompatible libraries, but not `useWindowVirtualizer`.
Both hooks are thin wrappers around the same internal
`useVirtualizerBase`, so they return the same referentially-stable
virtualizer instance. Its methods (e.g. `getVirtualItems()`,
`getTotalSize()`) return internally-mutated values rather than new ones,
which is exactly the "interior mutability" pattern that breaks
memoization described in the module type provider.
Because only `useVirtualizer` was registered, code using
`useWindowVirtualizer` is silently compiled with the same unsafe
memoization. In our app this froze `getVirtualItems()` to its
first-render value (an empty `[]` before measurement), producing
permanently-empty virtualized lists — the same class of bug #34493
fixed, just via the window variant.
This registers `useWindowVirtualizer` alongside `useVirtualizer` with
the identical shape, in **both** implementations:
-
`compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts`
-
`compiler/crates/react_compiler_hir/src/default_module_type_provider.rs`
(Rust port)
See also the community report referenced in #34493:
https://github.com/TanStack/virtual/issues/736#issuecomment-3065658277
## How did you test this change?
Each addition mirrors the existing `useVirtualizer` entry exactly (same
kind, params, return type, and `knownIncompatible` message shape); both
are data-only additions to the module type providers, following the
precedent set by #34493 which changed only the TS file. The Rust change
keeps the two providers in sync.
## Summary
In a large react app, especially when components having similar starting
names like Table, TableColumn, TableCell, TableRow all together 100+
components when rendered in a virtualized table. Traversing the search
result is sometimes difficult with scroll
The component tree search only let you step through matches one at a
time (Enter / Shift+Enter). In large apps with many similarly-named
components (Table, TableRow, TableCell, ...) a search can return 100+
matches in a virtualized list, making a specific match tedious to reach.
- the result counter is an editable, live-scrubbing
index field: typing a number scrolls to that match as you type
(clamped to range)
- Fixes re-search getting stuck, clearing the box and retyping the same
term while a match was still selected snapped back to that same
component. It now advances to the next match (find-next semantics).
## How did you test this change?
Adds a SearchableTable example to the DevTools shell and unit tests for
the new action and the retype behavior.
https://github.com/user-attachments/assets/7ea9801a-7bcb-4e8f-bf73-a5307a0fdbae
cc @hoxyq Let me know what do you feel about this feature, if its
helpful for devtools.
`printOperationsArray` in `react-devtools-shared` walks an operations
array under
the invariant that each `switch` case leaves `i` pointing at the next
opcode (loop
header at `packages/react-devtools-shared/src/utils.js`).
The `TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE` case broke that
invariant:
```js
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
i++; // skip opcode -> i now at the value slot
const activitySliceIDChange = operations[i + 1]; // reads the slot AFTER the value; i not advanced
...
}
```
The operation is exactly two slots, `[opcode, activitySliceID]` (see the
writer in
`packages/react-devtools-shared/src/backend/fiber/renderer.js`, which
pushes the
opcode then the id). So the case did two things wrong:
1. It logged the wrong number: `operations[i + 1]` reads the slot
*after* the
value (the next operation's opcode, or `undefined` at the end of the
array).
2. It left `i` pointing at the value slot, so the outer `while (i <
operations.length)` loop re-read the activity-slice id as an opcode. For
any
non-zero slice id that falls through to `default: throw
Error("Unsupported
Bridge operation ...")`, aborting the whole dump.
The two canonical decoders of this same operation both use the correct
pattern
(skip the opcode, then read *and* advance past the value):
- `devtools/store.js`: `i++; nextActivitySliceID = operations[i++];`
- `devtools/views/Profiler/CommitTreeBuilder.js`: `i++; const
activitySliceIDChange = operations[i++];`
This change makes `printOperationsArray` match them by reading
`operations[i++]`.
This is a debug-only diagnostic path: the only caller is the
`__DEBUG__`-guarded
dump in `backend/legacy/renderer.js`, so it is not a production crash.
The bug was
introduced in #34908.
## How did you test this change?
Added a regression test for `printOperationsArray` in
`packages/react-devtools-shared/src/__tests__/utils-test.js`. The
fixture chains
two activity-slice operations, `[rendererID, rootID, stringTableSize=0,
opcode, 42,
opcode, 0]`; the trailing operation is what forces the reader to advance
past the
first value slot rather than re-read it. It asserts the call does not
throw, logs
once, and that the message contains both `Applied activity slice change
to 42` and
`Reset applied activity slice`.
Ran the DevTools Jest project (built first, as that project requires a
build):
- With the fix: 51/51 pass, including the new test.
- Reverting only the one-line fix back to `operations[i + 1]` and
rebuilding: the
new test fails with `Unsupported Bridge operation "42"` (exactly the
predicted
failure), 50 pass / 1 fail. Restored the fix and it is green again.
`yarn prettier-check` and `yarn linc` are clean on the changed files.
`validateChildKeys` in `packages/react/src/jsx/ReactJSXElement.js` had a
signature
of `validateChildKeys(node, parentType)`, but #34174 ("Remove unused
arguments from
ReactElement") dropped the second argument, changing the signature to
`validateChildKeys(node)` and updating every call site to pass a single
argument.
That change removed several now-unused `@param` lines from the same
file, but left
one behind on `validateChildKeys`:
```js
/**
* ...
* @internal
* @param {ReactNode} node Statically passed child of any type.
* @param {*} parentType node's parent's type. // <- no such parameter anymore
*/
function validateChildKeys(node) {
```
`parentType` no longer appears anywhere in the function signature or
body, so this
`@param` line is stale and misleading to anyone reading the doc comment.
This PR
deletes that single line. The remaining `@param {ReactNode} node`
already fully and
correctly documents the sole parameter. No code or behavior change.
## How did you test this change?
This is a documentation-only change (a JSDoc comment on an `@internal`
helper), so
there is no runtime behavior to test. I verified it as follows:
- Confirmed `parentType` no longer appears anywhere in
`packages/react/src/jsx/ReactJSXElement.js` (`grep -n parentType`
returns no
matches after the change).
- Confirmed the signature `function validateChildKeys(node)` and all
call sites are
unchanged by this diff.
- `yarn prettier` (via `scripts/prettier/index.js check-changed`) -
clean.
- `yarn linc` (ESLint on changed files) - passed.
- `yarn flow dom-node` - No errors.
---
## Diff (for reference)
```diff
diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js
@@ -860,7 +860,6 @@ export function cloneElement(element, config, children) {
*
* @internal
* @param {ReactNode} node Statically passed child of any type.
- * @param {*} parentType node's parent's type.
*/
function validateChildKeys(node) {
```
`formatConsoleArguments` in
`packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js`
is used by the DevTools backend (via `hook.js`) to inline `console.*`
printf-style substitutions after stripping React's appended component
stack.
For `%s`/`%d`/`%i`/`%f` it consumes the next argument with
`args.splice(argumentsPointer, 1)` and formats the result.
When a format string has more specifiers than arguments, `splice`
returns an
empty array, so `arg` is `undefined` and the specifier is rendered as
text:
`%s` becomes `"undefined"`, and `%d`/`%i`/`%f` become `"NaN"`.
```js
formatConsoleArguments('%s %s', 'the');
// before: ['the undefined']
// after: ['the %s']
```
Browsers and Node's `util.format` leave an unmatched specifier as a
literal
(`console.log('%s %s', 'a')` prints `a %s`; `console.log('%d')` prints
`%d`).
So a message like `console.warn('value: %s')` was shown in DevTools as
`value: undefined` instead of `value: %s`.
This guards each of the `%d`/`%i`, `%f`, and `%s` cases on argument
availability (`argumentsPointer >= args.length`): when nothing is left
to
consume it keeps the specifier text and does not splice, mirroring the
existing trailing-`%` handling added in #36852. An explicitly passed
`undefined`/`null` argument is unchanged and still renders as
`undefined`/`null`, since a value is present at that position (the
`formats nullish values` test still passes).
## How did you test this change?
Added a regression test to the existing `formatConsoleArguments`
describe
block in `packages/react-devtools-shared/src/__tests__/utils-test.js`:
```js
it('keeps specifiers literal when no argument is supplied', () => {
expect(formatConsoleArguments('%s %s', 'the')).toEqual(['the %s']);
expect(formatConsoleArguments('%s %d', 'value')).toEqual(['value %d']);
expect(formatConsoleArguments('%s %i', 'value')).toEqual(['value %i']);
expect(formatConsoleArguments('%s %f', 'value')).toEqual(['value %f']);
});
```
Each assertion fails on `main` (it produces `['the undefined']` and
`['value NaN']`) and passes with the fix.
Commands run locally:
```
yarn test --build --project devtools packages/react-devtools-shared/src/__tests__/utils-test.js
# 51 passed, 51 total
yarn lint packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js \
packages/react-devtools-shared/src/__tests__/utils-test.js
# Lint passed.
yarn flow dom-node
# No errors!
yarn prettier-check packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js \
packages/react-devtools-shared/src/__tests__/utils-test.js
# clean
```
Cross-checked the expected output against Node `util.format`:
`util.format('%s %s', 'the')` -> `the %s`; `util.format('%d')` -> `%d`.
`formatConsoleArgumentsToSingleString` in
`packages/react-devtools-shared/src/backend/utils/index.js`
inlines `console.*` printf-style substitutions into a single string.
That string is
used both as the dedup key and as the displayed text for per-component
warnings/errors.
The `switch` that consumes the captured flag handles `s`, `d`, `i`, and
`f`, and the
function's own header comment says it "Implements s, d, i and f
placeholders". But
the substitution regex only captured `[jds]`:
```js
const REGEXP = /(%?)(%([jds]))/g;
```
So `%i` and `%f` were never matched. The `case 'i'` and `case 'f'` arms
were dead
code: the specifier was emitted literally and its argument was never
consumed. Worse,
because the unmatched specifier does not shift its argument, every
following specifier
in the same format string then binds to the wrong argument (a cascading
off-by-one
over the remaining args).
`%i` and `%f` are standard console integer/float specifiers (Node
`util.format` and
browsers both support them), so this affected common log formats. The
fix adds `i`
and `f` to the regex class so the existing switch arms run:
```js
const REGEXP = /(%?)(%([jdisf]))/g;
```
This is a one-character-class change that reconciles the regex with the
switch and
the header comment. The pre-existing behavior that `%j` is matched but
has no
`case` (so it falls through unchanged) is intentionally left as-is; it
is out of
scope for this fix.
## How did you test this change?
Added three regression tests to the existing
`formatConsoleArgumentsToSingleString`
describe block in
`packages/react-devtools-shared/src/__tests__/utils-test.js`:
- `formatConsoleArgumentsToSingleString('%i', 3.14)` -> `'3'`
- `formatConsoleArgumentsToSingleString('%f', 3.5)` -> `'3.5'`
- `formatConsoleArgumentsToSingleString('a %i b %s', 7, 'x')` -> `'a 7 b
x'` (locks
in argument alignment)
Commands run locally (experimental devtools bundles):
```
yarn build-for-devtools
yarn test --build --project=devtools -r=experimental packages/react-devtools-shared/src/__tests__/utils-test.js
```
Result: `Tests: 53 passed, 53 total`.
To confirm the tests actually cover the bug, I reverted the
one-character fix back to
`[jds]` and reran: the three new tests fail exactly as the bug predicts,
e.g. `%i`
yields `"%i 3.14"` and `a %i b %s` yields `"a %i b 7 x"` (the `%s` binds
to `7`
instead of `x`, showing the off-by-one). Restoring the fix makes them
pass again.
Also green:
```
yarn linc # ESLint on changed files: passed
yarn prettier # no files reflagged
yarn flow dom-node # No errors!
```
This PR adds a new `getParentStack` tool to the Facade and an
integration of it in `react-devtools-cdt-mcp`.
Similarly to the owner stack tool, it receives a `uid` of a specific
component and returns structural parent stack. For every node, it
includes `uid`, `name`, `type`.
Follow-up to #36903
The previous change stopped `onAllReady` from firing after a fatal shell
error by assigning `noop` to `request.onAllReady` inside `fatalError`.
This mutated a completion callback on the request as a side channel,
mirroring the existing `request.onShellError = noop` in `completeShell`.
This refactor removes both noop assignments and instead guards the calls
at the point where they would happen, so the callbacks on the request
stay the ones the caller passed in.
For `onAllReady`, `erroredTask` now returns right after `fatalError` in
the root (`boundary === null`) case, so it no longer falls through to
the `allPendingTasks === 0` check that calls `completeAll`. This matches
the sibling `finishAbortedTask`, which already returns after a fatal
root error. Later task completions cannot reach `completeAll` either,
because `performWork` bails out once the request status is past `OPEN`.
For `onShellError`, `fatalError` now decides whether to call it based on
the same condition that governed the assignment: the shell is complete
exactly when `request.pendingRootTasks === 0`, and `completeShell` only
runs at that point, so guarding the call is equivalent to the previous
nulling out. `onFatalError` still always fires because the error is
always fatal to the request.
This is a behavior-preserving refactor of the fix landed in #36903; the
tests added there continue to pass unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
Two fundamental changes (plus a lot of formatting that got mixed in :/
): Modify how we determine if a local binding exists for the case where
the scope is extracted from a function, rather than a module, which is
the case for snap tests. And, loosen the rust port tester to allow debug
output to be printed from one compiler or another as long as both sides
error in the same phase with the same error message (basically, the rust
port emits debug information before throwing an error, whereas the TS
version does one or the other -- but it's not actually a real
difference).
With this change and loosening, the Rust compiler conforms on the
todo-locally-require-fbt case.
---------
Co-authored-by: mvitousek <[email protected]>