Compare commits

..

71 Commits

Author SHA1 Message Date
Jorge Cabiedes
ca1d5e068b Fix flow 2025-06-02 12:56:24 -07:00
Jorge Cabiedes
ecb1861dd8 Fix lints 2025-06-02 11:16:36 -07:00
Jorge Cabiedes
a5861172c6 Gate __internal_only_getComponentTree definition 2025-06-02 11:09:42 -07:00
Jorge Cabiedes
df0a663a8c Remove unused code 2025-05-27 10:52:33 -07:00
Jorge Cabiedes
6c71a7766d Fix tests 2025-05-27 10:43:28 -07:00
Jorge Cabiedes
9275c835c3 Fix naming on react-devtools.js 2025-05-27 10:28:35 -07:00
Jorge Cabiedes
9cae1cea4e Remove getComponentTree() definition gating 2025-05-27 09:33:37 -07:00
Jorge Cabiedes
183bd4feac Fix CI 2025-05-27 09:15:24 -07:00
Jorge Cabiedes
81c3a5331e Error handling 2025-05-27 08:59:35 -07:00
Jorge Cabiedes
ab86a5efe8 Address comments 2025-05-27 08:54:30 -07:00
Jorge Cabiedes
1e4614bf13 Add IS_INTERNAL flag to eslintrc 2025-05-20 10:50:49 -07:00
Jorge Cabiedes
c5ab27a649 Add IS_INTERNAL build time flag and gate getComponentTree() 2025-05-20 10:44:16 -07:00
Jorge Cabiedes
049bfbb169 Add IS_INTERNAL build time flag and gate getComponentTree() 2025-05-20 10:43:14 -07:00
Jorge Cabiedes
a85b0b0bb4 Error handling 2025-05-19 11:27:45 -07:00
Jorge Cabiedes
789e5f02c5 fix url redefinition 2025-05-19 11:16:40 -07:00
Jorge Cabiedes
d6d929e2f1 More cleanup 2025-05-19 09:11:40 -07:00
Jorge Cabiedes
26315d64dc Cleanup React Devtools port attempt 2025-05-19 09:09:19 -07:00
Jorge Cabiedes
2852c9d08c Merge remote-tracking branch 'origin/main' into component-tree-tool 2025-05-19 09:03:55 -07:00
Jorge Cabiedes
94718f18b4 Add component tree function to devtools and finish adding componentTree mcp tool 2025-05-19 09:00:49 -07:00
Sebastian Markbåge
462d08f9ba Move SuspenseListProps into a shared/ReactTypes (#33298)
So they can be shared by server. Incorporates the types from definitely
typed too.
2025-05-17 20:00:56 -04:00
Sebastian Markbåge
6060367ef8 [Fizz] Wrap revealCompletedBoundaries in a ViewTransitions aware version (#33293)
When needed.

For the external runtime we always include this wrapper.

For others, we only include it if we have an ViewTransitions affecting.
If we discover the ViewTransitions late, then we can upgrade an already
emitted instruction.

This doesn't yet do anything useful with it, that's coming in a follow
up. This is just the mechanism for how it gets installed.
2025-05-17 18:18:24 -04:00
Sebastian Markbåge
c250b7d980 [Fizz] Should be considered complete inside onShellReady callback (#33295)
We decremented `allPendingTasks` after invoking `onShellReady`. Which
means that in that scope it wasn't considered fully complete.

Since the pattern for flushing in Node.js is to start piping in
`onShellReady` and that's how you can get sync behavior, this led us to
think that we had more work left to do. For example we emitted the
`writeShellTimeInstruction` in this scenario before.
2025-05-16 14:53:40 -04:00
Jan Kassens
4448b18760 [eslint-plugin-react-hooks] fix exhaustive deps lint rule with component syntax (#33182) 2025-05-15 12:51:18 -04:00
Ricky
4a45ba92c4 [sync] Fix noop for xplat (#33214)
Noop detection for xplat syncs broke because `eslint-plugin-react-hooks`
uses versions like:

- `0.0.0-experimental-d85f86cf-20250514`

But xplat expects them to be of the form:

- `19.2.0-native-fb-63d664b2-20250514`

This PR fixes the noop by ignoring
`eslint-plugin-react-hooks/package.json` changes. This means we won't
create a sync if only that package.json changes, but that should be rare
and we can follow up with better detection if needed.

[Example failed
action](https://github.com/facebook/react/actions/runs/15032346805/job/42247414406):

<img width="1031" alt="Screenshot 2025-05-15 at 11 31 17 AM"
src="https://github.com/user-attachments/assets/d902079c-1afe-4e18-af1d-25e60e28929e"
/>

I believe the regression was caused by
https://github.com/facebook/react/pull/33104
2025-05-15 12:12:51 -04:00
lauren
08cb2d7ee7 [ci] Log author_association (#33213)
For debugging purposes, log author_association
2025-05-15 11:49:56 -04:00
lauren
203df2c940 [compiler] Update changelog for 19.1.0-rc.2 (#33207)
Update the changelog.
2025-05-15 10:34:11 -04:00
Sebastian Markbåge
65b5aae010 [Fizz] Add vt- prefix attributes to annotate <ViewTransition> in HTML (#33206)
Stacked on #33194 and #33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will
be responsible for animating the reveal if necessary (not in this PR).
However, for the future runtime to know what to do it needs to know
about the `<ViewTransition>` configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could
model them as comments like we do with other virtual nodes like Suspense
and Activity. However, that doesn't let us target them with
querySelector and CSS (for no-JS transitions). We also don't have to
model every ViewTransition since not every combination can happen using
only the server runtime. So instead this collapses `<ViewTransition>`
and applies the configuration to the inner DOM nodes.

```js
<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>
```

Becomes:

```html
<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>
```

I use `vt-` prefix as opposed to `data-` to keep these virtual
attributes away from user specific ones but we're effectively claiming
this namespace.

There are four triggers `vt-update`, `vt-enter`, `vt-exit` and
`vt-share`. The server resolves which ones might apply to this DOM node.
The value represents the class name (after resolving
view-transition-type mappings) or `"auto"` if no specific class name is
needed but this is still a trigger.

The value can also be `"none"`. This is different from missing because
for example an `vt-update="none"` will block mutations inside it from
triggering the boundary where as a missing `vt-update` would bubble up
to be handled by a parent.

`vt-name` is technically only necessary when `vt-share` is specified to
find a pair. However, since an explicit name can also be used to target
specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

`vt-enter` can only affect the first DOM node inside a Suspense
boundary's content since the reveal would cause it to enter but nothing
deeper inside. Similarly `vt-exit` can only affect the first DOM node
inside a fallback. So for every other case we can exclude them. (For
future MPA ViewTransitions of the whole document it might also be
something we annotate to children inside the `<body>` as well.) Ideally
we'd only include `vt-enter` for Suspense boundaries that actually
flushed a fallback but since we prepare all that content earlier it's
hard to know.

`vt-share` can be anywhere inside an fallback or content. Technically we
don't have to include it outside the root most Suspense boundary or for
boundaries that are inlined into the root shell. However, this is tricky
to detect. It would also not be correct for future MPA ViewTransitions
because in that case the shared scenario can affect anything in the two
documents so it needs to be in every node everywhere which is
effectively what we do. If a `share` class is specified but it has no
explicit name, we can exclude it since it can't match anything.

`vt-update` is only necessary if something below or a sibling might
update like a Suspense boundary. However, since we don't know when
rendering a segment if it'll later asynchronously add a Suspense
boundary later we have to assume that anywhere might have a child. So
these are always included. We collapse to use the inner most one when
directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack
of virtual nodes.
2025-05-15 01:04:10 -04:00
Sebastian Markbåge
3f67d0857e [Fizz] Track whether we're in a fallback on FormatContext (#33194)
Removes the `isFallback` flag on Tasks and tracks it on the
formatContext instead.

Less memory and avoids passing and tracking extra arguments to all the
pushStartInstance branches that doesn't need it.

We'll need to be able to track more Suspense related contexts on this
for View Transitions anyway.
2025-05-15 00:06:06 -04:00
Sebastian Markbåge
96eb84e493 Claim the useId name space for every auto named ViewTransition (#33200)
This is a partial revert of #33094. It's true that we don't need the
server and client ViewTransition names to line up. However the server
does need to be able to generate deterministic names for itself. The
cheapest way to do that is using the useId algorithm. When it's used by
the server, the client needs to also materialize an ID even if it
doesn't use it.
2025-05-14 17:52:41 -04:00
Sebastian Markbåge
63d664b220 Don't consider Portals animating unless they're wrapped in a ViewTransition (#33191)
And that doesn't disable with `update="none"`.

The principle here is that we want the content of a Portal to animate if
other things are animating with it but if other things aren't animating
then we don't.
2025-05-14 17:50:56 -04:00
Jan Kassens
d85f86cf01 Delete stray file (#33199)
Not sure where this was coming from.
2025-05-14 11:27:36 -04:00
Jorge Cabiedes Acosta
76dddd1d57 Port complete 2025-05-13 16:05:41 -07:00
Sebastian Markbåge
3a5b326d81 [Fiber] Trigger default indicator for isomorphic async actions with no root associated (#33190)
Stacked on #33160, #33162, #33186 and #33188.

We have a special case that's awkward for default indicators. When you
start a new async Transition from `React.startTransition` then there's
not yet any associated root with the Transition because you haven't
necessarily `setState` on anything yet until the promise resolves.
That's what `entangleAsyncAction` handles by creating a lane that
everything entangles with until all async actions are done.

If there are no sync updates before the end of the event, we should
trigger a default indicator until either the async action completes
without update or if it gets entangled with some roots we should keep it
going until those roots are done.
2025-05-13 16:10:28 -04:00
Sebastian Markbåge
59440424d0 Implement Navigation API backed default indicator for DOM renderer (#33162)
Stacked on #33160.

By default, if `onDefaultTransitionIndicator` is not overridden, this
will trigger a fake Navigation event using the Navigation API. This is
intercepted to create an on-going navigation until we complete the
Transition. Basically each default Transition is simulated as a
Navigation.

This triggers the native browser loading state (in Chrome at least). So
now by default the browser spinner spins during a Transition if no other
loading state is provided. Firefox and Safari hasn't shipped Navigation
API yet and even in the flag Safari has, it doesn't actually trigger the
native loading state.

To ensures that you can still use other Navigations concurrently, we
don't start our fake Navigation if there's one on-going already.
Similarly if our fake Navigation gets interrupted by another. We wait
for on-going ones to finish and then start a new fake one if we're
supposed to be still pending.

There might be other routers on the page that might listen to intercept
Navigation Events. Typically you'd expect them not to trigger a refetch
when navigating to the same state. However, if they want to detect this
we provide the `"react-transition"` string in the `info` field for this
purpose.
2025-05-13 16:00:38 -04:00
Sebastian Markbåge
b480865db0 [Fiber] Always flush Default priority in the microtask if a Transition was scheduled (#33186)
Stacked on #33160.

The purpose of this is to avoid calling `onDefaultTransitionIndicator`
when a Default priority update acts as the loading indicator, but still
call it when unrelated Default updates happens nearby.

When we schedule Default priority work that gets batched with other
events in the same frame more or less. This helps optimize by doing less
work. However, that batching means that we can't separate work from one
setState from another. If we would consider all Default priority work in
a frame when determining whether to show the default we might never show
it in cases like when you have a recurring timer updating something.

This instead flushes the Default priority work eagerly along with the
sync work at the end of the event, if this event scheduled any
Transition work. This is then used to determine if the default indicator
needs to be shown.
2025-05-13 15:52:44 -04:00
Sebastian Markbåge
62d3f36ea7 [Fiber] Trigger default transition indicator if needed (#33160)
Stacked on #33159.

This implements `onDefaultTransitionIndicator`.

The sequence is:

1) In `markRootUpdated` we schedule Transition updates as needing
`indicatorLanes` on the root. This tracks the lanes that currently need
an indicator to either start or remain going until this lane commits.
2) Track mutations during any commit. We use the same hook that view
transitions use here but instead of tracking it just per view transition
scope, we also track a global boolean for the whole root.
3) If a sync/default commit had any mutations, then we clear the
indicator lane for the `currentEventTransitionLane`. This requires that
the lane is still active while we do these commits. See #33159. In other
words, a sync update gets associated with the current transition and it
is assumed to be rendering the loading state for that corresponding
transition so we don't need a default indicator for this lane.
4) At the end of `processRootScheduleInMicrotask`, right before we're
about to enter a new "event transition lane" scope, it is no longer
possible to render any more loading states for the current transition
lane. That's when we invoke `onDefaultTransitionIndicator` for any roots
that have new indicator lanes.
5) When we commit, we remove the finished lanes from `indicatorLanes`
and once that reaches zero again, then we can clean up the default
indicator. This approach means that you can start multiple different
transitions while an indicator is still going but it won't stop/restart
each time. Instead, it'll wait until all are done before stopping.

Follow ups:

- [x] Default updates are currently not enough to cancel because those
aren't flush in the same microtask. That's unfortunate. #33186
- [x] Handle async actions before the setState. Since these don't
necessarily have a root this is tricky. #33190
- [x] Disable for `useDeferredValue`. ~Since it also goes through
`markRootUpdated` and schedules a Transition lane it'll get a default
indicator even though it probably shouldn't have one.~ EDIT: Turns out
this just works because it doesn't go through `markRootUpdated` when
work is left behind.
- [x] Implement built-in DOM version by default. #33162
2025-05-13 15:45:11 -04:00
Sebastian Markbåge
0cac32d60d [Fiber] Stash the entangled async action lane on currentEventTransitionLane (#33188)
When we're entangled with an async action lane we use that lane instead
of the currentEventTransitionLane. Conversely, if we start a new async
action lane we reuse the currentEventTransitionLane.

So they're basically supposed to be in sync but they're not if you
resolve the async action and then schedule new stuff in the same event.
Then you end up with two transitions in the same event with different
lanes.

By stashing it like this we fix that but it also gives us an opportunity
to check just the currentEventTransitionLane to see if this event
scheduled any regular Transition updates or Async Transitions.
2025-05-13 15:20:59 -04:00
Sebastian Markbåge
676f0879f3 Reset currentEventTransitionLane after flushing sync work (#33159)
This keeps track of the transition lane allocated for this event. I want
to be able to use the current one within sync work flushing to know
which lane needs its loading indicator cleared.

It's also a bit weird that transition work scheduled inside sync updates
in the same event aren't entangled with other transitions in that event
when `flushSync` is.

Therefore this moves it to reset after flushing.

It should have no impact. Just splitting it out into a separate PR for
an abundance of caution.

The only thing this might affect would be if the React internals throws
and it doesn't reset after. But really it doesn't really have to reset
and they're all entangled anyway.
2025-05-13 15:18:02 -04:00
Sebastian Markbåge
997c7bc930 [DevTools] Get source location from structured callsites in prepareStackTrace (#33143)
When we get the source location for "View source for this element" we
should be using the enclosing function of the callsite of the child. So
that we don't just point to some random line within the component.

This is similar to the technique in #33136.

This technique is now really better than the fake throw technique, when
available. So I now favor the owner technique. The only problem it's
only available in DEV and only if it has a child that's owned (and not
filtered).

We could implement this same technique for the error that's thrown in
the fake throwing solution. However, we really shouldn't need that at
all because for client components we should be able to call
`inspect(fn)` at least in Chrome which is even better.
2025-05-13 12:39:10 -04:00
Sebastian Markbåge
b94603b955 [Fizz] Gate rel="expect" behind enableFizzBlockingRender (#33183)
Enabled in experimental channel.

We know this is critical semantics to enforce at the HTML level since if
you don't then you can't add explicit boundaries after the fact.
However, this might have to go in a major release to allow for
upgrading.
2025-05-13 10:17:53 -04:00
Jenny Steele
2bcf06b692 [ReactFlightWebpackPlugin] Add support for .mjs file extension (#33028)
## Summary
Our builds generate files with a `.mjs` file extension. These are
currently filtered out by `ReactFlightWebpackPlugin` so I am updating it
to support this file extension.

This fixes https://github.com/facebook/react/issues/33155

## How did you test this change?
I built the plugin with this change and used `yalc` to test it in my
project. I confirmed the expected files now show up in
`react-client-manifest.json`
2025-05-12 21:16:15 -04:00
Samuel Susla
5d04d73274 Add eager alternate.stateNode cleanup (#33161)
This is a fix for a problem where React retains shadow nodes longer than
it needs to. The behaviour is shown in React Native test:
https://github.com/facebook/react-native/blob/main/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js#L169

# Problem
When React commits a new shadow tree, old shadow nodes are stored inside
`fiber.alternate.stateNode`. This is not cleared up until React clones
the node again. This may be problematic if mutation deletes a subtree,
in that case `fiber.alternate.stateNode` will retain entire subtree
until next update. In case of image nodes, this means retaining entire
images.

So when React goes from revision A: `<View><View /></View>` to revision
B: `<View />`, `fiber.alternate.stateNode` will be pointing to Shadow
Node that represents revision A..


![image](https://github.com/user-attachments/assets/076b677e-d152-4763-8c9d-4f923212b424)


# Fix
To fix this, this PR adds a new feature flag
`enableEagerAlternateStateNodeCleanup`. When enabled,
`alternate.stateNode` is proactively pointed towards finishedWork's
stateNode, releasing resources sooner.

I have verified this fixes the issue [demonstrated by React Native
tests](https://github.com/facebook/react-native/blob/main/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js#L169).
All existing React tests pass when the flag is enabled.
2025-05-12 17:39:20 +01:00
mofeiZ
3820740a7f [compiler][entrypoint] Fix edgecases for noEmit and opt-outs (#33148)
Title
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33148).
* #33149
* __->__ #33148
2025-05-09 13:37:49 -04:00
mofeiZ
5069e18060 [compiler][be] Make program traversal more readable (#33147)
React Compiler's program traversal logic is pretty lengthy and complex
as we've added a lot of features piecemeal. `compileProgram` is 300+
lines long and has confusing control flow (defining helpers inline,
invoking visitors, mutating-asts-while-iterating, mutating global
`ALREADY_COMPILED` state).

- Moved more stuff to `ProgramContext`
- Separated `compileProgram` into a bunch of helpers

Tested by syncing this stack to a Meta codebase and observing no
compilation output changes (D74487851, P1806855669, P1806855379)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33147).
* #33149
* #33148
* __->__ #33147
2025-05-09 13:23:08 -04:00
Sebastian Markbåge
21fdf308a1 Use a shared noop function from shared/noop (#33154)
Stacked on #33150.

We use `noop` functions in a lot of places as place holders. I don't
think there's any real optimizations we get from having separate
instances. This moves them to use a common instance in `shared/noop`.
2025-05-08 21:33:18 -04:00
Jack Pope
4ca97e4891 Clean up enableSiblingPrerendering flag (#32319) 2025-05-08 20:49:23 -04:00
Sebastian Markbåge
9b79292ae7 Add plumbing for onDefaultTransitionIndicator (#33150)
This just adds the options at the root and wire it up to the root but it
doesn't do anything yet.
2025-05-08 20:42:50 -04:00
Niklas Mollenhauer
ac06829246 feat(compiler): Implement constant propagation for template literals (#33139)
New take on #29716

## Summary
Template literals consisting entirely of constant values will be inlined
to a string literal, effectively replacing the backticks with a double
quote.

This is done primarily to make the resulting instruction a string
literal, so it can be processed further in constant propatation. So this
is now correctly simplified to `true`:
```js
`` === "" // now true
`a${1}` === "a1" // now true
```

If a template string literal can only partially be comptime-evaluated,
it is not that useful for dead code elimination or further constant
folding steps and thus, is left as-is in that case. Same is true if the
literal contains an array, object, symbol or function.

## How did you test this change?

See added tests.
2025-05-08 09:24:22 -07:00
mofeiZ
38ef6550a8 [compiler][playground][tests] Standardize more pragmas (#33146)
(Almost) all pragmas are now one of the following:
- `@...TestOnly`: custom pragma for test fixtures
- `@<configName>` | `@<configName>:true`: enables with either true or a
default enabled value
- `@<configName>:<json value>`
2025-05-08 11:26:53 -04:00
mofeiZ
b629a865fb [compiler][be] Move test pragma to separate file (#33145)
`Environment.ts` is getting complex so let's separate test / playground
parsing logic from it
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33145).
* #33146
* __->__ #33145
2025-05-08 11:24:44 -04:00
mofeiZ
fbe7bc21b9 [compiler][be] repro edge cases for noEmit and module opt-outs (#33144)
see test fixtures
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33144).
* #33146
* #33145
* __->__ #33144
2025-05-08 11:18:16 -04:00
Dawid Małecki
9518f11856 Root import types from react-native in ReactNativeTypes (#33063) 2025-05-08 12:12:35 +01:00
Ruslan Lesiutin
557a64795c React DevTools 6.1.1 -> 6.1.2 (#33142)
Patch release to mitigate https://github.com/facebook/react/issues/32659

Essentially just 6.1.1 with:
* Restore all Transitions for Tree updates
([eps1lon](https://github.com/eps1lon) in
[#33042](https://github.com/facebook/react/pull/33042))
* Restore "double-click to view owners tree" functionality
([eps1lon](https://github.com/eps1lon) in
[#33039](https://github.com/facebook/react/pull/33039))
2025-05-08 08:01:17 +01:00
Jorge Cabiedes
a75932b2ea Port relevant logic from react devtools 2025-05-07 16:37:34 -07:00
Jorge Cabiedes Acosta
8fa3dfc845 Smarter Devtools integration 2025-05-07 14:39:04 -07:00
Jack Pope
8a8df5dbdd Add dispatchEvent to fragment instances (#32813)
`fragmentInstance.dispatchEvent(evt)` calls `element.dispatchEvent(evt)`
on the fragment's host parent. This mimics bubbling if the
`fragmentInstance` could receive an event itself.

If the parent is disconnected, there is a dev warning and no event is
dispatched.
2025-05-07 14:00:59 -04:00
Niklas Mollenhauer
946da518eb feat(compiler): implement constant folding for unary minus (#33140)
## Summary
`-constant` is represented as a `UnaryExpression` node that is currently
not part of constant folding. If the operand is a constant number, the
node is folded to `constant * -1`. This also coerces `-0` to `0`,
resulting in `0 === -0` being folded to `true`.

## How did you test this change?
See attached tests
2025-05-07 10:15:11 -07:00
Sebastian Markbåge
a437c99ff7 [Flight] Clarify that location field is a FunctionLocation not a CallSite (#33141)
Follow up to #33136.

This clarifies in the types where the conversion happens from a CallSite
which we use to simulate getting the enclosing line/col to a
FunctionLocation which doesn't represent a CallSite but actually just
the function which only has an enclosing line/col.
2025-05-07 13:02:41 -04:00
Jack Pope
4206fe4982 Allow fragment refs to attempt focus/focusLast on nested host children (#33058)
This enables `focus` and `focusLast` methods on FragmentInstances to
search nested host components, depth first. Attempts focus on each child
and bails if one is successful. Previously, only the first level of host
children would attempt focus.

Now if we have an example like

```
component MenuItem() {
  return (<div><a>{...}</a></div>)
}

component Menu() {
  return <Fragment>{items.map(i => <MenuItem i={i} />)}</Fragment>
}
```
We can target focus on the first or last a tag, rather than checking
each wrapping div and then noop.
2025-05-07 12:47:28 -04:00
Sebastian Markbåge
4a702865dd [Flight] Encode enclosing line/column numbers and use it to align the fake function (#33136)
Stacked on #33135.

This encodes the line/column of the enclosing function as part of the
stack traces. When that information is available.

I adjusted the fake function code generation so that the beginning of
the arrow function aligns with these as much as possible.

This ensures that when the browser tries to look up the line/column of
the enclosing function, such as for getting the function name, it gets
the right one. If we can't get the enclosing line/column, then we encode
it at the beginning of the file. This is likely to get a miss in the
source map identifiers, which means that the function name gets
extracted from the runtime name instead which is better.

Another thing where this is used is the in the Performance Track.
Ideally that would be fixed by
https://issues.chromium.org/u/1/issues/415968771 but the enclosing
information is useful for other things like the function name resolution
anyway.

We can also use this for the "View source for this element" in React
DevTools.
2025-05-07 12:34:55 -04:00
Sebastian Markbåge
0ff1d13b80 [Flight] Parse Stack Trace from Structured CallSite if available (#33135)
This is first step to include more enclosing line/column in the parsed
data.

We install our own `prepareStackTrace` to collect structured callsite
data and only fall back to parsing the string if it was already
evaluated or if `prepareStackTrace` doesn't work in this environment.

We still mirror the default V8 format for encoding the function name
part. A lot of this is covered by tests already.
2025-05-07 11:43:37 -04:00
YongSeok Jang (장용석)
53c9f81049 [DevTools] Use Popover API for TraceUpdates highlighting (#32614)
## Summary

When using React DevTools to highlight component updates, the highlights
would sometimes appear behind elements that use the browser's
[top-layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer)
(such as `<dialog>` elements or components using the Popover API). This
made it difficult to see which components were updating when they were
inside or behind top-layer elements.

This PR fixes the issue by using the Popover API to ensure that
highlighting appears on top of all content, including elements in the
top-layer. The implementation maintains backward compatibility with
browsers that don't support the Popover API.

## How did you test this change?

I tested this change in the following ways:

1. Manually tested in Chrome (which supports the Popover API) with:
- Created a test application with React components inside `<dialog>`
elements and custom elements using the Popover API
- Verified that component highlighting appears above these elements when
they update
- Confirmed that highlighting displays correctly for nested components
within top-layer elements

2. Verified backward compatibility:
- Tested in browsers without Popover API support to ensure fallback
behavior works correctly
- Confirmed that no errors occur and highlighting still functions as
before

3. Ran the React DevTools test suite:
   - All tests pass successfully
   - No regressions were introduced

[demo-page](https://devtools-toplayer-demo.vercel.app/)
[demo-repo](https://github.com/yongsk0066/devtools-toplayer-demo)

### AS-IS

https://github.com/user-attachments/assets/dc2e1281-969f-4f61-82c3-480153916969

### TO-BE

https://github.com/user-attachments/assets/dd52ce35-816c-42f0-819b-0d5d0a8a21e5
2025-05-07 15:48:17 +01:00
Jack Pope
e5a8de81e5 Add compareDocumentPosition to fragment instances (#32722)
This adds `compareDocumentPosition(otherNode)` to fragment instances.

The semantics implemented are meant to match typical element
positioning, with some fragment specifics. See the unit tests for all
expectations.

- An element preceding a fragment is `Node.DOCUMENT_POSITION_PRECEDING`
- An element after a fragment is `Node.DOCUMENT_POSITION_FOLLOWING`
- An element containing the fragment is
`Node.DOCUMENT_POSITION_PRECEDING` and
`Node.DOCUMENT_POSITION_CONTAINING`
- An element within the fragment is
`Node.DOCUMENT_POSITION_CONTAINED_BY`
- An element compared against an empty fragment will result in
`Node.DOCUMENT_POSITION_DISCONNECTED` and
`Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`

Since we assume a fragment instances target children are DOM siblings
and we want to compare the full fragment as a pseudo container, we can
compare against the first target child outside of handling the special
cases (empty fragments and contained elements).
2025-05-06 13:01:40 -04:00
Jorge Cabiedes
7a2c7045ae [mcp] Add proper web-vitals metric collection (#33109)
Multiple things here:
- Improve the mean calculation for metrics so we don't report 0 when
web-vitals fail to be retrieved
- improve ui chaos monkey to use puppeteer APIs since only those trigger
INP/CLS metrics since we need emulated mouse clicks
- Add logic to navigate to a temp page after render since some
web-vitals metrics are only calculated when the page is backgrounded
- Some readability improvements
2025-05-06 08:50:40 -07:00
Jorge Cabiedes Acosta
0e5c79cfea Bruteforcing react devtools 2025-05-06 08:21:12 -07:00
Sebastian Markbåge
845d93742f Remove useId semantics from View Transition name generation (#33094)
Originally I thought it was important that SSR used the same View
Transition name as the client so that the Fizz runtime could emit those
names and then the client could pick up and take over. However, I no
longer believe that approach is feasible. Instead, the names can be
generated only during that particular animation.

Therefore we can simplify the auto name assignment to not have to
consider the hydration.
2025-05-06 10:33:03 -04:00
Sebastian Markbåge
54a50729cc [Fiber] Replay events between commits (#33130)
Stacked on #33129. Flagged behind `enableHydrationChangeEvent`.

If you type into a controlled input before hydration and something else
rerenders like a setState in an effect, then the controlled input will
reset to whatever React thought it was. Even with event replaying that
this is stacked on, if the second render happens before event replaying
has fired in a separate task.

We don't want to flush inside the commit phase because then things like
flushSync in these events wouldn't work since they're inside the commit
stack.

This flushes all event replaying between renders by flushing it at the
end of `flushSpawned` work. We've already committed at that point and is
about to either do subsequent renders or yield to event loop for passive
effects which could have these events fired anyway. This just ensures
that they've already happened by the time subsequent renders fire. This
means that there's now a type of event that fire between sync render
passes.
2025-05-06 00:23:27 -04:00
Sebastian Markbåge
587cb8f896 [Fiber] Replay onChange Events if input/textarea/select has changed before hydration (#33129)
This fixes a long standing issue that controlled inputs gets out of sync
with the browser state if it's changed before we hydrate.

This resolves the issue by replaying the change events (click, input and
change) if the value has changed by the time we commit the hydration.
That way you can reflect the new value in state to bring it in sync. It
does this whether controlled or uncontrolled.

The idea is that this should be ok to replay because it's similar to the
continuous events in that it doesn't replay a sequence but only reflects
the current state of the tree.

Since this is a breaking change I added it behind
`enableHydrationChangeEvent` flag.

There is still an additional issue remaining that I intend to address in
a follow up. If a `useLayoutEffect` triggers an sync rerender on
hydration (always a bad idea) then that can rerender before we have had
a chance to replay the change events. If that renders through a input
then that input will always override the browser value with the
controlled value. Which will reset it before we've had a change to
update to the new value.
2025-05-06 00:10:05 -04:00
Matt Carroll
79586c7eb6 Add test for multiple form submissions (#33059)
Test for #30041 and #33055
2025-05-05 14:47:47 -07:00
Jack Pope
edf550b679 Ship enableFabricCompleteRootInCommitPhase (#33064)
This was shipped internally. Cleaning up the flag.
2025-05-05 13:36:44 -04:00
Sebastian "Sebbie" Silbermann
b9cfa0d308 [Flight] Prevent serialized size leaking across requests (#33121) 2025-05-05 18:30:33 +02:00
381 changed files with 8183 additions and 3259 deletions

View File

@@ -496,6 +496,7 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@@ -504,6 +505,7 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},
@@ -579,6 +581,7 @@ module.exports = {
JSONValue: 'readonly',
JSResourceReference: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
React$AbstractComponent: 'readonly',
@@ -634,5 +637,6 @@ module.exports = {
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
navigation: 'readonly',
},
};

View File

@@ -15,6 +15,7 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}

View File

@@ -332,10 +332,10 @@ jobs:
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
echo "===================="
# Ignore REVISION or lines removing @generated headers.
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else

View File

@@ -15,6 +15,7 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}

View File

@@ -17,6 +17,7 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}

View File

@@ -1,3 +1,9 @@
## 19.1.0-rc.2 (May 14, 2025)
## babel-plugin-react-compiler
* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)
## 19.1.0-rc.1 (April 21, 2025)
## eslint-plugin-react-hooks

View File

@@ -1,5 +1,5 @@
import { c as _c } from "react/compiler-runtime"; // 
        @compilationMode(all)
        @compilationMode:"all"
function nonReactFn() {
  const $ = _c(1);
  let t0;

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
function nonReactFn() {
  return {};
}

View File

@@ -92,7 +92,7 @@ function useFoo(propVal: {+baz: number}) {
},
{
name: 'compilationMode-infer',
input: `// @compilationMode(infer)
input: `// @compilationMode:"infer"
function nonReactFn() {
return {};
}
@@ -101,7 +101,7 @@ function nonReactFn() {
},
{
name: 'compilationMode-all',
input: `// @compilationMode(all)
input: `// @compilationMode:"all"
function nonReactFn() {
return {};
}

View File

@@ -18,8 +18,9 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {CompilerReactTarget} from './Options';
import {getReactCompilerRuntimeModule} from './Program';
import {LoggerEvent, PluginOptions} from './Options';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
export function validateRestrictedImports(
path: NodePath<t.Program>,
@@ -52,32 +53,65 @@ export function validateRestrictedImports(
}
}
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: PluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
};
export class ProgramContext {
/* Program and environment context */
/**
* Program and environment context
*/
scope: BabelScope;
opts: PluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;
hookPattern: string | null;
suppressions: Array<SuppressionRange>;
hasModuleScopeOptOut: boolean;
/*
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
* consistently respect the `skip()` function to avoid revisiting a node within
* a pass, so we use this set to track nodes that we have compiled.
*/
alreadyCompiled: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
// known generated or referenced identifiers in the program
knownReferencedNames: Set<string> = new Set();
// generated imports
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
constructor(
program: NodePath<t.Program>,
reactRuntimeModule: CompilerReactTarget,
hookPattern: string | null,
) {
this.hookPattern = hookPattern;
/**
* Metadata from compilation
*/
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
inferredEffectLocations: Set<t.SourceLocation> = new Set();
constructor({
program,
suppressions,
opts,
filename,
code,
hasModuleScopeOptOut,
}: ProgramContextOptions) {
this.scope = program.scope;
this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
this.opts = opts;
this.filename = filename;
this.code = code;
this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
this.suppressions = suppressions;
this.hasModuleScopeOptOut = hasModuleScopeOptOut;
}
isHookName(name: string): boolean {
if (this.hookPattern == null) {
if (this.opts.environment.hookPattern == null) {
return isHookName(name);
} else {
const match = new RegExp(this.hookPattern).exec(name);
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
return (
match != null && typeof match[1] === 'string' && isHookName(match[1])
);
@@ -179,6 +213,12 @@ export class ProgramContext {
});
return Err(error);
}
logEvent(event: LoggerEvent): void {
if (this.opts.logger != null) {
this.opts.logger.logEvent(this.filename, event);
}
}
}
function getExistingImports(

View File

@@ -98,7 +98,7 @@ export type PluginOptions = {
* provided rules will skip compilation. To disable this feature (never bailout of compilation
* even if the default ESLint is suppressed), pass an empty array.
*/
eslintSuppressionRules?: Array<string> | null | undefined;
eslintSuppressionRules: Array<string> | null | undefined;
flowSuppressions: boolean;
/*
@@ -106,7 +106,7 @@ export type PluginOptions = {
*/
ignoreUseNoForget: boolean;
sources?: Array<string> | ((filename: string) => boolean) | null;
sources: Array<string> | ((filename: string) => boolean) | null;
/**
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.

View File

@@ -12,7 +12,7 @@ import {
CompilerErrorDetail,
ErrorSeverity,
} from '../CompilerError';
import {EnvironmentConfig, ReactFunctionType} from '../HIR/Environment';
import {ReactFunctionType} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
import {isHookDeclaration} from '../Utils/HookDeclaration';
@@ -43,17 +43,21 @@ export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
export function findDirectiveEnablingMemoization(
directives: Array<t.Directive>,
): Array<t.Directive> {
return directives.filter(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
): t.Directive | null {
return (
directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
) ?? null
);
}
export function findDirectiveDisablingMemoization(
directives: Array<t.Directive>,
): Array<t.Directive> {
return directives.filter(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
): t.Directive | null {
return (
directives.find(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
) ?? null
);
}
@@ -88,13 +92,16 @@ export type CompileResult = {
function logError(
err: unknown,
pass: CompilerPass,
context: {
opts: PluginOptions;
filename: string | null;
},
fnLoc: t.SourceLocation | null,
): void {
if (pass.opts.logger) {
if (context.opts.logger) {
if (err instanceof CompilerError) {
for (const detail of err.details) {
pass.opts.logger.logEvent(pass.filename, {
context.opts.logger.logEvent(context.filename, {
kind: 'CompileError',
fnLoc,
detail: detail.options,
@@ -108,7 +115,7 @@ function logError(
stringifiedError = err?.toString() ?? '[ null ]';
}
pass.opts.logger.logEvent(pass.filename, {
context.opts.logger.logEvent(context.filename, {
kind: 'PipelineError',
fnLoc,
data: stringifiedError,
@@ -118,13 +125,17 @@ function logError(
}
function handleError(
err: unknown,
pass: CompilerPass,
context: {
opts: PluginOptions;
filename: string | null;
},
fnLoc: t.SourceLocation | null,
): void {
logError(err, pass, fnLoc);
logError(err, context, fnLoc);
if (
pass.opts.panicThreshold === 'all_errors' ||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
context.opts.panicThreshold === 'all_errors' ||
(context.opts.panicThreshold === 'critical_errors' &&
isCriticalError(err)) ||
isConfigError(err) // Always throws regardless of panic threshold
) {
throw err;
@@ -187,7 +198,6 @@ export function createNewFunctionNode(
}
}
// Avoid visiting the new transformed version
ALREADY_COMPILED.add(transformedFn);
return transformedFn;
}
@@ -239,13 +249,6 @@ function insertNewOutlinedFunctionNode(
}
}
/*
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
* consistently respect the `skip()` function to avoid revisiting a node within
* a pass, so we use this set to track nodes that we have compiled.
*/
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
const DEFAULT_ESLINT_SUPPRESSIONS = [
'react-hooks/exhaustive-deps',
'react-hooks/rules-of-hooks',
@@ -268,41 +271,43 @@ function isFilePartOfSources(
return false;
}
export type CompileProgramResult = {
export type CompileProgramMetadata = {
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
inferredEffectLocations: Set<t.SourceLocation>;
};
/**
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
* exceptions thrown by this function will fail the babel build.
* - call `handleError` if your error is recoverable.
* Unless the error is a warning / info diagnostic, compilation of a function
* / entire file should also be skipped.
* - throw an exception if the error is fatal / not recoverable.
* Examples of this are invalid compiler configs or failure to codegen outlined
* functions *after* already emitting optimized components / hooks that invoke
* the outlined functions.
* Main entrypoint for React Compiler.
*
* @param program The Babel program node to compile
* @param pass Compiler configuration and context
* @returns Compilation results or null if compilation was skipped
*/
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass,
): CompileProgramResult | null {
): CompileProgramMetadata | null {
/**
* This is directly invoked by the react-compiler babel plugin, so exceptions
* thrown by this function will fail the babel build.
* - call `handleError` if your error is recoverable.
* Unless the error is a warning / info diagnostic, compilation of a function
* / entire file should also be skipped.
* - throw an exception if the error is fatal / not recoverable.
* Examples of this are invalid compiler configs or failure to codegen outlined
* functions *after* already emitting optimized components / hooks that invoke
* the outlined functions.
*/
if (shouldSkipCompilation(program, pass)) {
return null;
}
const environment = pass.opts.environment;
const restrictedImportsErr = validateRestrictedImports(program, environment);
const restrictedImportsErr = validateRestrictedImports(
program,
pass.opts.environment,
);
if (restrictedImportsErr) {
handleError(restrictedImportsErr, pass, null);
return null;
}
const programContext = new ProgramContext(
program,
pass.opts.target,
environment.hookPattern,
);
/*
* Record lint errors and critical errors as depending on Forget's config,
* we may still need to run Forget's analysis on every function (even if we
@@ -313,16 +318,102 @@ export function compileProgram(
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
pass.opts.flowSuppressions,
);
const queue: Array<{
kind: 'original' | 'outlined';
fn: BabelFn;
fnType: ReactFunctionType;
}> = [];
const programContext = new ProgramContext({
program: program,
opts: pass.opts,
filename: pass.filename,
code: pass.code,
suppressions,
hasModuleScopeOptOut:
findDirectiveDisablingMemoization(program.node.directives) != null,
});
const queue: Array<CompileSource> = findFunctionsToCompile(
program,
pass,
programContext,
);
const compiledFns: Array<CompileResult> = [];
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(current.fn, current.fnType, programContext);
if (compiled != null) {
for (const outlined of compiled.outlined) {
CompilerError.invariant(outlined.fn.outlined.length === 0, {
reason: 'Unexpected nested outlined functions',
loc: outlined.fn.loc,
});
const fn = insertNewOutlinedFunctionNode(
program,
current.fn,
outlined.fn,
);
fn.skip();
programContext.alreadyCompiled.add(fn.node);
if (outlined.type !== null) {
queue.push({
kind: 'outlined',
fn,
fnType: outlined.type,
});
}
}
compiledFns.push({
kind: current.kind,
originalFn: current.fn,
compiledFn: compiled,
});
}
}
// Avoid modifying the program if we find a program level opt-out
if (programContext.hasModuleScopeOptOut) {
if (compiledFns.length > 0) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
loc: null,
}),
);
handleError(error, programContext, null);
}
return null;
}
// Insert React Compiler generated functions into the Babel AST
applyCompiledFunctions(program, compiledFns, pass, programContext);
return {
retryErrors: programContext.retryErrors,
inferredEffectLocations: programContext.inferredEffectLocations,
};
}
type CompileSource = {
kind: 'original' | 'outlined';
fn: BabelFn;
fnType: ReactFunctionType;
};
/**
* Find all React components and hooks that need to be compiled
*
* @returns An array of React functions from @param program to transform
*/
function findFunctionsToCompile(
program: NodePath<t.Program>,
pass: CompilerPass,
programContext: ProgramContext,
): Array<CompileSource> {
const queue: Array<CompileSource> = [];
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
const fnType = getReactFunctionType(fn, pass, environment);
if (fnType === null || ALREADY_COMPILED.has(fn.node)) {
const fnType = getReactFunctionType(fn, pass);
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
return;
}
@@ -331,7 +422,7 @@ export function compileProgram(
* traversal will loop infinitely.
* Ensure we avoid visiting the original function again.
*/
ALREADY_COMPILED.add(fn.node);
programContext.alreadyCompiled.add(fn.node);
fn.skip();
queue.push({kind: 'original', fn, fnType});
@@ -346,7 +437,6 @@ export function compileProgram(
* can reference `this` which is unsafe for compilation
*/
node.skip();
return;
},
ClassExpression(node: NodePath<t.ClassExpression>) {
@@ -355,7 +445,6 @@ export function compileProgram(
* can reference `this` which is unsafe for compilation
*/
node.skip();
return;
},
FunctionDeclaration: traverseFunction,
@@ -370,205 +459,206 @@ export function compileProgram(
filename: pass.filename ?? null,
},
);
const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
const inferredEffectLocations = new Set<t.SourceLocation>();
const processFn = (
fn: BabelFn,
fnType: ReactFunctionType,
): null | CodegenFunction => {
let optInDirectives: Array<t.Directive> = [];
let optOutDirectives: Array<t.Directive> = [];
if (fn.node.body.type === 'BlockStatement') {
optInDirectives = findDirectiveEnablingMemoization(
fn.node.body.directives,
);
optOutDirectives = findDirectiveDisablingMemoization(
fn.node.body.directives,
);
}
return queue;
}
/**
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
* Program node itself. We need to figure out whether an eslint suppression range
* applies to this function first.
*/
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
suppressions,
fn,
);
let compileResult:
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown};
if (suppressionsInFunction.length > 0) {
compileResult = {
kind: 'error',
error: suppressionsToCompilerError(suppressionsInFunction),
};
/**
* Try to compile a source function, taking into account all local suppressions,
* opt-ins, and opt-outs.
*
* Errors encountered during compilation are either logged (if recoverable) or
* thrown (if non-recoverable).
*
* @returns the compiled function or null if the function was skipped (due to
* config settings and/or outputs)
*/
function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
): null | CodegenFunction {
let directives;
if (fn.node.body.type !== 'BlockStatement') {
directives = {optIn: null, optOut: null};
} else {
directives = {
optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
};
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
try {
compileResult = {
kind: 'compile',
compiledFn: compileFn(
fn,
environment,
fnType,
'all_features',
programContext,
pass.opts.logger,
pass.filename,
pass.code,
),
};
} catch (err) {
compileResult = {kind: 'error', error: err};
}
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
if (compileResult.kind === 'error') {
/**
* If an opt out directive is present, log only instead of throwing and don't mark as
* containing a critical error.
*/
if (optOutDirectives.length > 0) {
logError(compileResult.error, pass, fn.node.loc ?? null);
} else {
handleError(compileResult.error, pass, fn.node.loc ?? null);
}
// If non-memoization features are enabled, retry regardless of error kind
if (
!(environment.enableFire || environment.inferEffectDependencies != null)
) {
return null;
}
try {
compileResult = {
kind: 'compile',
compiledFn: compileFn(
fn,
environment,
fnType,
'no_inferred_memo',
programContext,
pass.opts.logger,
pass.filename,
pass.code,
),
};
if (
!compileResult.compiledFn.hasFireRewrite &&
!compileResult.compiledFn.hasInferredEffect
) {
return null;
}
} catch (err) {
// TODO: we might want to log error here, but this will also result in duplicate logging
if (err instanceof CompilerError) {
retryErrors.push({fn, error: err});
}
return null;
}
}
/**
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
* unused 'use no forget/memo' directive.
*/
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
for (const directive of optOutDirectives) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSkip',
fnLoc: fn.node.body.loc ?? null,
reason: `Skipped due to '${directive.value.value}' directive.`,
loc: directive.loc ?? null,
});
}
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSuccess',
fnLoc: fn.node.loc ?? null,
fnName: compileResult.compiledFn.id?.name ?? null,
memoSlots: compileResult.compiledFn.memoSlotsUsed,
memoBlocks: compileResult.compiledFn.memoBlocks,
memoValues: compileResult.compiledFn.memoValues,
prunedMemoBlocks: compileResult.compiledFn.prunedMemoBlocks,
prunedMemoValues: compileResult.compiledFn.prunedMemoValues,
/**
* If 'use no forget/memo' is present and we still ran the code through the
* compiler for validation, log a skip event and don't mutate the babel AST.
* This allows us to flag if there is an unused 'use no forget/memo'
* directive.
*/
if (
programContext.opts.ignoreUseNoForget === false &&
directives.optOut != null
) {
programContext.logEvent({
kind: 'CompileSkip',
fnLoc: fn.node.body.loc ?? null,
reason: `Skipped due to '${directives.optOut.value}' directive.`,
loc: directives.optOut.loc ?? null,
});
return null;
}
programContext.logEvent({
kind: 'CompileSuccess',
fnLoc: fn.node.loc ?? null,
fnName: compiledFn.id?.name ?? null,
memoSlots: compiledFn.memoSlotsUsed,
memoBlocks: compiledFn.memoBlocks,
memoValues: compiledFn.memoValues,
prunedMemoBlocks: compiledFn.prunedMemoBlocks,
prunedMemoValues: compiledFn.prunedMemoValues,
});
/**
* Always compile functions with opt in directives.
*/
if (optInDirectives.length > 0) {
return compileResult.compiledFn;
} else if (pass.opts.compilationMode === 'annotation') {
/**
* No opt-in directive in annotation mode, so don't insert the compiled function.
*/
return null;
}
if (!pass.opts.noEmit) {
return compileResult.compiledFn;
}
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.noEmit) {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
* dependencies.
*/
for (const loc of compileResult.compiledFn.inferredEffectLocations) {
if (loc !== GeneratedSource) inferredEffectLocations.add(loc);
}
return null;
};
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(current.fn, current.fnType);
if (compiled === null) {
continue;
}
for (const outlined of compiled.outlined) {
CompilerError.invariant(outlined.fn.outlined.length === 0, {
reason: 'Unexpected nested outlined functions',
loc: outlined.fn.loc,
});
const fn = insertNewOutlinedFunctionNode(
program,
current.fn,
outlined.fn,
);
fn.skip();
ALREADY_COMPILED.add(fn.node);
if (outlined.type !== null) {
queue.push({
kind: 'outlined',
fn,
fnType: outlined.type,
});
for (const loc of compiledFn.inferredEffectLocations) {
if (loc !== GeneratedSource) {
programContext.inferredEffectLocations.add(loc);
}
}
compiledFns.push({
kind: current.kind,
compiledFn: compiled,
originalFn: current.fn,
});
return null;
} else if (
programContext.opts.compilationMode === 'annotation' &&
directives.optIn == null
) {
/**
* If no opt-in directive is found and the compiler is configured in
* annotation mode, don't insert the compiled function.
*/
return null;
} else {
return compiledFn;
}
}
function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
/**
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
* Program node itself. We need to figure out whether an eslint suppression range
* applies to this function first.
*/
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
programContext.suppressions,
fn,
);
if (suppressionsInFunction.length > 0) {
return {
kind: 'error',
error: suppressionsToCompilerError(suppressionsInFunction),
};
}
/**
* Do not modify source if there is a module scope level opt out directive.
*/
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
program.node.directives,
);
if (moduleScopeOptOutDirectives.length > 0) {
try {
return {
kind: 'compile',
compiledFn: compileFn(
fn,
programContext.opts.environment,
fnType,
'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
),
};
} catch (err) {
return {kind: 'error', error: err};
}
}
/**
* If non-memo feature flags are enabled, retry compilation with a more minimal
* feature set.
*
* @returns a CodegenFunction if retry was successful
*/
function retryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
): CodegenFunction | null {
const environment = programContext.opts.environment;
if (
!(environment.enableFire || environment.inferEffectDependencies != null)
) {
return null;
}
/*
* Only insert Forget-ified functions if we have not encountered a critical
* error elsewhere in the file, regardless of bailout mode.
/**
* Note that function suppressions are not checked in the retry pipeline, as
* they only affect auto-memoization features.
*/
try {
const retryResult = compileFn(
fn,
environment,
fnType,
'no_inferred_memo',
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
);
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
return null;
}
return retryResult;
} catch (err) {
// TODO: we might want to log error here, but this will also result in duplicate logging
if (err instanceof CompilerError) {
programContext.retryErrors.push({fn, error: err});
}
return null;
}
}
/**
* Applies React Compiler generated functions to the babel AST by replacing
* existing functions in place or inserting new declarations.
*/
function applyCompiledFunctions(
program: NodePath<t.Program>,
compiledFns: Array<CompileResult>,
pass: CompilerPass,
programContext: ProgramContext,
): void {
const referencedBeforeDeclared =
pass.opts.gating != null
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
@@ -576,6 +666,7 @@ export function compileProgram(
for (const result of compiledFns) {
const {kind, originalFn, compiledFn} = result;
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
programContext.alreadyCompiled.add(transformedFn);
if (referencedBeforeDeclared != null && kind === 'original') {
CompilerError.invariant(pass.opts.gating != null, {
@@ -598,7 +689,6 @@ export function compileProgram(
if (compiledFns.length > 0) {
addImportsToProgram(program, programContext);
}
return {retryErrors, inferredEffectLocations};
}
function shouldSkipCompilation(
@@ -640,14 +730,10 @@ function shouldSkipCompilation(
function getReactFunctionType(
fn: BabelFn,
pass: CompilerPass,
/**
* TODO(mofeiZ): remove once we validate PluginOptions with Zod
*/
environment: EnvironmentConfig,
): ReactFunctionType | null {
const hookPattern = environment.hookPattern;
const hookPattern = pass.opts.environment.hookPattern;
if (fn.node.body.type === 'BlockStatement') {
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}

View File

@@ -18,7 +18,7 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {Environment} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramResult} from './Program';
import {CompileProgramMetadata} from './Program';
function throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
@@ -109,7 +109,7 @@ export default function validateNoUntransformedReferences(
filename: string | null,
logger: Logger | null,
env: EnvironmentConfig,
compileResult: CompileProgramResult | null,
compileResult: CompileProgramMetadata | null,
): void {
const moduleLoadChecks = new Map<
string,
@@ -236,7 +236,7 @@ function transformProgram(
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
filename: string | null,
logger: Logger | null,
compileResult: CompileProgramResult | null,
compileResult: CompileProgramMetadata | null,
): void {
const traversalState: TraversalState = {
shouldInvalidateScopes: true,

View File

@@ -9,15 +9,7 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {
CompilationMode,
defaultOptions,
Logger,
PanicThresholdOptions,
parsePluginOptions,
PluginOptions,
ProgramContext,
} from '../Entrypoint';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -158,7 +150,7 @@ export type Hook = z.infer<typeof HookSchema>;
* missing some recursive Object / Function shapeIds
*/
const EnvironmentConfigSchema = z.object({
export const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).default(new Map()),
/**
@@ -640,191 +632,6 @@ const EnvironmentConfigSchema = z.object({
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
/**
* For test fixtures and playground only.
*
* Pragmas are straightforward to parse for boolean options (`:true` and
* `:false`). These are 'enabled' config values for non-boolean configs (i.e.
* what is used when parsing `:true`).
*/
const testComplexConfigDefaults: PartialEnvironmentConfig = {
validateNoCapitalizedCalls: [],
enableChangeDetectionForDebugging: {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
},
enableEmitFreeze: {
source: 'react-compiler-runtime',
importSpecifierName: 'makeReadOnly',
},
enableEmitInstrumentForget: {
fn: {
source: 'react-compiler-runtime',
importSpecifierName: 'useRenderCounter',
},
gating: {
source: 'react-compiler-runtime',
importSpecifierName: 'shouldInstrument',
},
globalGating: 'DEV',
},
enableEmitHookGuards: {
source: 'react-compiler-runtime',
importSpecifierName: '$dispatcherGuard',
},
inlineJsxTransform: {
elementSymbol: 'react.transitional.element',
globalDevVar: 'DEV',
},
lowerContextAccess: {
source: 'react-compiler-runtime',
importSpecifierName: 'useContext_withSelector',
},
inferEffectDependencies: [
{
function: {
source: 'react',
importSpecifierName: 'useEffect',
},
numRequiredArgs: 1,
},
{
function: {
source: 'shared-runtime',
importSpecifierName: 'useSpecialEffect',
},
numRequiredArgs: 2,
},
{
function: {
source: 'useEffectWrapper',
importSpecifierName: 'default',
},
numRequiredArgs: 1,
},
],
};
/**
* For snap test fixtures and playground only.
*/
function parseConfigPragmaEnvironmentForTest(
pragma: string,
): EnvironmentConfig {
const maybeConfig: any = {};
// Get the defaults to programmatically check for boolean properties
const defaultConfig = EnvironmentConfigSchema.parse({});
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
let [key, val = undefined] = keyVal.split(':');
const isSet = val === undefined || val === 'true';
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] =
testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
continue;
}
if (key === 'customMacros' && val) {
const valSplit = val.split('.');
if (valSplit.length > 0) {
const props = [];
for (const elt of valSplit.slice(1)) {
if (elt === '*') {
props.push({type: 'wildcard'});
} else if (elt.length > 0) {
props.push({type: 'name', name: elt});
}
}
maybeConfig[key] = [[valSplit[0], props]];
}
continue;
}
if (
key !== 'enableResetCacheOnSourceFileChanges' &&
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
) {
// skip parsing non-boolean properties
continue;
}
if (val === undefined || val === 'true') {
maybeConfig[key] = true;
} else {
maybeConfig[key] = false;
}
}
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
if (config.success) {
/**
* Unless explicitly enabled, do not insert HMR handling code
* in test fixtures or playground to reduce visual noise.
*/
if (config.data.enableResetCacheOnSourceFileChanges == null) {
config.data.enableResetCacheOnSourceFileChanges = false;
}
return config.data;
}
CompilerError.invariant(false, {
reason: 'Internal error, could not parse config from pragma string',
description: `${fromZodError(config.error)}`,
loc: null,
suggestions: null,
});
}
export function parseConfigPragmaForTests(
pragma: string,
defaults: {
compilationMode: CompilationMode;
},
): PluginOptions {
const environment = parseConfigPragmaEnvironmentForTest(pragma);
let compilationMode: CompilationMode = defaults.compilationMode;
let panicThreshold: PanicThresholdOptions = 'all_errors';
let noEmit: boolean = defaultOptions.noEmit;
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
switch (token) {
case '@compilationMode(annotation)': {
compilationMode = 'annotation';
break;
}
case '@compilationMode(infer)': {
compilationMode = 'infer';
break;
}
case '@compilationMode(all)': {
compilationMode = 'all';
break;
}
case '@compilationMode(syntax)': {
compilationMode = 'syntax';
break;
}
case '@panicThreshold(none)': {
panicThreshold = 'none';
break;
}
case '@noEmit': {
noEmit = true;
break;
}
}
}
return parsePluginOptions({
environment,
compilationMode,
panicThreshold,
noEmit,
});
}
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
export type ReactFunctionType = 'Component' | 'Hook' | 'Other';

View File

@@ -17,7 +17,6 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
export {computeDominatorTree, computePostDominatorTree} from './Dominator';
export {
Environment,
parseConfigPragmaForTests,
validateEnvironmentConfig,
type EnvironmentConfig,
type ExternalFunction,

View File

@@ -327,6 +327,23 @@ function evaluateInstruction(
}
return null;
}
case '-': {
const operand = read(constants, value.value);
if (
operand !== null &&
operand.kind === 'Primitive' &&
typeof operand.value === 'number'
) {
const result: Primitive = {
kind: 'Primitive',
value: operand.value * -1,
loc: value.loc,
};
instr.value = result;
return result;
}
return null;
}
default:
return null;
}
@@ -492,6 +509,73 @@ function evaluateInstruction(
}
return null;
}
case 'TemplateLiteral': {
if (value.subexprs.length === 0) {
const result: InstructionValue = {
kind: 'Primitive',
value: value.quasis.map(q => q.cooked).join(''),
loc: value.loc,
};
instr.value = result;
return result;
}
if (value.subexprs.length !== value.quasis.length - 1) {
return null;
}
if (value.quasis.some(q => q.cooked === undefined)) {
return null;
}
let quasiIndex = 0;
let resultString = value.quasis[quasiIndex].cooked as string;
++quasiIndex;
for (const subExpr of value.subexprs) {
const subExprValue = read(constants, subExpr);
if (!subExprValue || subExprValue.kind !== 'Primitive') {
return null;
}
const expressionValue = subExprValue.value;
if (
typeof expressionValue !== 'number' &&
typeof expressionValue !== 'string' &&
typeof expressionValue !== 'boolean' &&
!(typeof expressionValue === 'object' && expressionValue === null)
) {
// value is not supported (function, object) or invalid (symbol), or something else
return null;
}
const suffix = value.quasis[quasiIndex].cooked;
++quasiIndex;
if (suffix === undefined) {
return null;
}
/*
* Spec states that concat calls ToString(argument) internally on its parameters
* -> we don't have to implement ToString(argument) ourselves and just use the engine implementation
* Refs:
* - https://tc39.es/ecma262/2024/#sec-tostring
* - https://tc39.es/ecma262/2024/#sec-string.prototype.concat
* - https://tc39.es/ecma262/2024/#sec-template-literals-runtime-semantics-evaluation
*/
resultString = resultString.concat(expressionValue as string, suffix);
}
const result: InstructionValue = {
kind: 'Primitive',
value: resultString,
loc: value.loc,
};
instr.value = result;
return result;
}
case 'LoadLocal': {
const placeValue = read(constants, value.place);
if (placeValue !== null) {

View File

@@ -0,0 +1,210 @@
/**
* 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 {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {
CompilationMode,
defaultOptions,
parsePluginOptions,
PluginOptions,
} from '../Entrypoint';
import {EnvironmentConfig} from '..';
import {
EnvironmentConfigSchema,
PartialEnvironmentConfig,
} from '../HIR/Environment';
import {Err, Ok, Result} from './Result';
import {hasOwnProperty} from './utils';
function tryParseTestPragmaValue(val: string): Result<unknown, unknown> {
try {
let parsedVal: unknown;
const stringMatch = /^"([^"]*)"$/.exec(val);
if (stringMatch && stringMatch.length > 1) {
parsedVal = stringMatch[1];
} else {
parsedVal = JSON.parse(val);
}
return Ok(parsedVal);
} catch (e) {
return Err(e);
}
}
const testComplexConfigDefaults: PartialEnvironmentConfig = {
validateNoCapitalizedCalls: [],
enableChangeDetectionForDebugging: {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
},
enableEmitFreeze: {
source: 'react-compiler-runtime',
importSpecifierName: 'makeReadOnly',
},
enableEmitInstrumentForget: {
fn: {
source: 'react-compiler-runtime',
importSpecifierName: 'useRenderCounter',
},
gating: {
source: 'react-compiler-runtime',
importSpecifierName: 'shouldInstrument',
},
globalGating: 'DEV',
},
enableEmitHookGuards: {
source: 'react-compiler-runtime',
importSpecifierName: '$dispatcherGuard',
},
inlineJsxTransform: {
elementSymbol: 'react.transitional.element',
globalDevVar: 'DEV',
},
lowerContextAccess: {
source: 'react-compiler-runtime',
importSpecifierName: 'useContext_withSelector',
},
inferEffectDependencies: [
{
function: {
source: 'react',
importSpecifierName: 'useEffect',
},
numRequiredArgs: 1,
},
{
function: {
source: 'shared-runtime',
importSpecifierName: 'useSpecialEffect',
},
numRequiredArgs: 2,
},
{
function: {
source: 'useEffectWrapper',
importSpecifierName: 'default',
},
numRequiredArgs: 1,
},
],
};
/**
* For snap test fixtures and playground only.
*/
function parseConfigPragmaEnvironmentForTest(
pragma: string,
): EnvironmentConfig {
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const valIdx = keyVal.indexOf(':');
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
const isSet = val === undefined || val === 'true';
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
continue;
}
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] = testComplexConfigDefaults[key];
} else if (isSet) {
maybeConfig[key] = true;
} else if (val === 'false') {
maybeConfig[key] = false;
} else if (val) {
const parsedVal = tryParseTestPragmaValue(val).unwrap();
if (key === 'customMacros' && typeof parsedVal === 'string') {
const valSplit = parsedVal.split('.');
const props = [];
for (const elt of valSplit.slice(1)) {
if (elt === '*') {
props.push({type: 'wildcard'});
} else if (elt.length > 0) {
props.push({type: 'name', name: elt});
}
}
maybeConfig[key] = [[valSplit[0], props]];
continue;
}
maybeConfig[key] = parsedVal;
}
}
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
if (config.success) {
/**
* Unless explicitly enabled, do not insert HMR handling code
* in test fixtures or playground to reduce visual noise.
*/
if (config.data.enableResetCacheOnSourceFileChanges == null) {
config.data.enableResetCacheOnSourceFileChanges = false;
}
return config.data;
}
CompilerError.invariant(false, {
reason: 'Internal error, could not parse config from pragma string',
description: `${fromZodError(config.error)}`,
loc: null,
suggestions: null,
});
}
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
gating: {
source: 'ReactForgetFeatureFlag',
importSpecifierName: 'isForgetEnabled_Fixtures',
},
};
export function parseConfigPragmaForTests(
pragma: string,
defaults: {
compilationMode: CompilationMode;
},
): PluginOptions {
const environment = parseConfigPragmaEnvironmentForTest(pragma);
const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',
compilationMode: defaults.compilationMode,
environment,
};
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const idx = keyVal.indexOf(':');
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
if (!hasOwnProperty(defaultOptions, key)) {
continue;
}
const isSet = val === undefined || val === 'true';
if (isSet && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (isSet) {
options[key] = true;
} else if (val === 'false') {
options[key] = false;
} else if (val != null) {
const parsedVal = tryParseTestPragmaValue(val).unwrap();
if (key === 'target' && parsedVal === 'donotuse_meta_internal') {
options[key] = {
kind: parsedVal,
runtimeModule: 'react',
};
} else {
options[key] = parsedVal;
}
}
}
return parsePluginOptions(options);
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
const Test = () => <div />;
export const FIXTURE_ENTRYPOINT = {
@@ -15,7 +15,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
const Test = () => {
const $ = _c(1);
let t0;

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
const Test = () => <div />;
export const FIXTURE_ENTRYPOINT = {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {
@@ -22,7 +22,7 @@ class Component {
## Code
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableEmitInstrumentForget @compilationMode(annotation)
// @enableEmitInstrumentForget @compilationMode:"annotation"
function Bar(props) {
'use forget';
@@ -24,7 +24,7 @@ function Foo(props) {
```javascript
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
function Bar(props) {
"use forget";

View File

@@ -1,4 +1,4 @@
// @enableEmitInstrumentForget @compilationMode(annotation)
// @enableEmitInstrumentForget @compilationMode:"annotation"
function Bar(props) {
'use forget';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @flow @compilationMode(infer)
// @flow @compilationMode:"infer"
export default component Foo(bar: number) {
return <Bar bar={bar} />;
}

View File

@@ -1,4 +1,4 @@
// @flow @compilationMode(infer)
// @flow @compilationMode:"infer"
export default component Foo(bar: number) {
return <Bar bar={bar} />;
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableEmitInstrumentForget @compilationMode(annotation)
// @enableEmitInstrumentForget @compilationMode:"annotation"
import {identity} from 'shared-runtime';
@@ -35,7 +35,7 @@ import {
shouldInstrument as _shouldInstrument3,
useRenderCounter,
} from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
import { identity } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @enableEmitInstrumentForget @compilationMode(annotation)
// @enableEmitInstrumentForget @compilationMode:"annotation"
import {identity} from 'shared-runtime';

View File

@@ -0,0 +1,136 @@
## Input
```javascript
import {Stringify, identity} from 'shared-runtime';
function foo() {
try {
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
} catch {}
return (
<Stringify
value={[
`` === '',
`\n` === '\n',
`a\nb`,
`\n`,
`a${1}b`,
` abc \u0041\n\u000a`,
`abc${1}def`,
`abc${1}def${2}`,
`abc${1}def${2}ghi`,
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
`1${2}${Math.sin(0)}`,
`${NaN}`,
`${Infinity}`,
`${-Infinity}`,
`${Number.MAX_SAFE_INTEGER}`,
`${Number.MIN_SAFE_INTEGER}`,
`${Number.MAX_VALUE}`,
`${Number.MIN_VALUE}`,
`${-0}`,
`
`,
`${{}}`,
`${[1, 2, 3]}`,
`${true}`,
`${false}`,
`${null}`,
`${undefined}`,
`123456789${0}`,
`${0}123456789`,
`${0}123456789${0}`,
`${0}1234${5}6789${0}`,
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
`${`${`${`${0}`}`}`}`,
`${`${`${`${''}`}`}`}`,
`${`${`${`${identity('')}`}`}`}`,
]}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
isComponent: false,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, identity } from "shared-runtime";
function foo() {
const $ = _c(1);
try {
identity(`${Symbol("0")}`);
} catch {}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<Stringify
value={[
true,
true,
"a\nb",
"\n",
"a1b",
" abc A\n\n\u0167",
"abc1def",
"abc1def2",
"abc1def2ghi",
"a4bcde6f",
`1${2}${Math.sin(0)}`,
`${NaN}`,
`${Infinity}`,
`${-Infinity}`,
`${Number.MAX_SAFE_INTEGER}`,
`${Number.MIN_SAFE_INTEGER}`,
`${Number.MAX_VALUE}`,
`${Number.MIN_VALUE}`,
"0",
"\n ",
`${{}}`,
`${[1, 2, 3]}`,
"true",
"false",
"null",
`${undefined}`,
"1234567890",
"0123456789",
"01234567890",
"01234567890",
"0123401234567890123456789067890",
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
"0",
"",
`${`${`${`${identity("")}`}`}`}`,
]}
/>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
isComponent: false,
};
```
### Eval output
(kind: ok) <div>{"value":[true,true,"a\nb","\n","a1b"," abc A\n\nŧ","abc1def","abc1def2","abc1def2ghi","a4bcde6f","120","NaN","Infinity","-Infinity","9007199254740991","-9007199254740991","1.7976931348623157e+308","5e-324","0","\n ","[object Object]","1,2,3","true","false","null","undefined","1234567890","0123456789","01234567890","01234567890","0123401234567890123456789067890","012340123456789067890","0","",""]}</div>

View File

@@ -0,0 +1,56 @@
import {Stringify, identity} from 'shared-runtime';
function foo() {
try {
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
} catch {}
return (
<Stringify
value={[
`` === '',
`\n` === '\n',
`a\nb`,
`\n`,
`a${1}b`,
` abc \u0041\n\u000a`,
`abc${1}def`,
`abc${1}def${2}`,
`abc${1}def${2}ghi`,
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
`1${2}${Math.sin(0)}`,
`${NaN}`,
`${Infinity}`,
`${-Infinity}`,
`${Number.MAX_SAFE_INTEGER}`,
`${Number.MIN_SAFE_INTEGER}`,
`${Number.MAX_VALUE}`,
`${Number.MIN_VALUE}`,
`${-0}`,
`
`,
`${{}}`,
`${[1, 2, 3]}`,
`${true}`,
`${false}`,
`${null}`,
`${undefined}`,
`123456789${0}`,
`${0}123456789`,
`${0}123456789${0}`,
`${0}1234${5}6789${0}`,
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
`${`${`${`${0}`}`}`}`,
`${`${`${`${''}`}`}`}`,
`${`${`${`${identity('')}`}`}`}`,
]}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
isComponent: false,
};

View File

@@ -0,0 +1,74 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
function foo() {
const a = -1;
return (
<Stringify
value={[
2 * a,
-0,
0 === -0,
-Infinity,
-NaN,
a * NaN,
a * Infinity,
a * -Infinity,
]}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
isComponent: false,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function foo() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<Stringify
value={[
-2,
0,
true,
-Infinity,
-NaN,
-1 * NaN,
-1 * Infinity,
-1 * -Infinity,
]}
/>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
isComponent: false,
};
```
### Eval output
(kind: ok) <div>{"value":[-2,0,true,null,null,null,null,null]}</div>

View File

@@ -0,0 +1,25 @@
import {Stringify} from 'shared-runtime';
function foo() {
const a = -1;
return (
<Stringify
value={[
2 * a,
-0,
0 === -0,
-Infinity,
-NaN,
a * NaN,
a * Infinity,
a * -Infinity,
]}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
isComponent: false,
};

View File

@@ -58,7 +58,7 @@ function foo() {
n0: true,
n1: false,
n2: false,
n3: !-1,
n3: false,
s0: true,
s1: false,
s2: false,

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @eslintSuppressionRules(my-app/react-rule)
// @eslintSuppressionRules:["my-app","react-rule"]
/* eslint-disable my-app/react-rule */
function lowercasecomponent() {
@@ -19,7 +19,7 @@ function lowercasecomponent() {
## Error
```
1 | // @eslintSuppressionRules(my-app/react-rule)
1 | // @eslintSuppressionRules:["my-app","react-rule"]
2 |
> 3 | /* eslint-disable my-app/react-rule */
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3)

View File

@@ -1,4 +1,4 @@
// @eslintSuppressionRules(my-app/react-rule)
// @eslintSuppressionRules:["my-app","react-rule"]
/* eslint-disable my-app/react-rule */
function lowercasecomponent() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender @compilationMode(infer)
// @validateRefAccessDuringRender @compilationMode:"infer"
function Component({ref}) {
const value = ref.current;
return <div>{value}</div>;
@@ -14,7 +14,7 @@ function Component({ref}) {
## Error
```
1 | // @validateRefAccessDuringRender @compilationMode(infer)
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
2 | function Component({ref}) {
> 3 | const value = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)

View File

@@ -1,4 +1,4 @@
// @validateRefAccessDuringRender @compilationMode(infer)
// @validateRefAccessDuringRender @compilationMode:"infer"
function Component({ref}) {
const value = ref.current;
return <div>{value}</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender @compilationMode(infer)
// @validateRefAccessDuringRender @compilationMode:"infer"
function Component(props) {
const value = props.ref.current;
return <div>{value}</div>;
@@ -14,7 +14,7 @@ function Component(props) {
## Error
```
1 | // @validateRefAccessDuringRender @compilationMode(infer)
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
2 | function Component(props) {
> 3 | const value = props.ref.current;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)

View File

@@ -1,4 +1,4 @@
// @validateRefAccessDuringRender @compilationMode(infer)
// @validateRefAccessDuringRender @compilationMode:"infer"
function Component(props) {
const value = props.ref.current;
return <div>{value}</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender @compilationMode(infer)
// @validateRefAccessDuringRender @compilationMode:"infer"
function Component(props) {
const ref = props.ref;
ref.current = true;

View File

@@ -1,4 +1,4 @@
// @validateRefAccessDuringRender @compilationMode(infer)
// @validateRefAccessDuringRender @compilationMode:"infer"
function Component(props) {
const ref = props.ref;
ref.current = true;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
function Component() {
return <Foo />;

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
function Component() {
return <Foo />;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateBlocklistedImports(DangerousImport)
// @validateBlocklistedImports:["DangerousImport"]
import {foo} from 'DangerousImport';
import {useIdentity} from 'shared-runtime';
@@ -17,7 +17,7 @@ function useHook() {
## Error
```
1 | // @validateBlocklistedImports(DangerousImport)
1 | // @validateBlocklistedImports:["DangerousImport"]
> 2 | import {foo} from 'DangerousImport';
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Bailing out due to blocklisted import. Import from module DangerousImport (2:2)
3 | import {useIdentity} from 'shared-runtime';

View File

@@ -1,4 +1,4 @@
// @validateBlocklistedImports(DangerousImport)
// @validateBlocklistedImports:["DangerousImport"]
import {foo} from 'DangerousImport';
import {useIdentity} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
import {useEffect, useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';
@@ -43,7 +43,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
import { useEffect, useMemo, useState } from "react";
import { ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
import {useEffect, useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer) @enableResetCacheOnSourceFileChanges
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
import {useEffect, useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';
@@ -46,7 +46,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) @enableResetCacheOnSourceFileChanges
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
import { useEffect, useMemo, useState } from "react";
import { ValidateMemoization } from "shared-runtime";
@@ -63,12 +63,12 @@ function unsafeUpdateConst() {
function Component() {
const $ = _c(3);
if (
$[0] !== "8d7015668f857996c3d895a7a90e3e16b8a791d5b9cd13f2c76e1c254aeedebb"
$[0] !== "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7"
) {
for (let $i = 0; $i < 3; $i += 1) {
$[$i] = Symbol.for("react.memo_cache_sentinel");
}
$[0] = "8d7015668f857996c3d895a7a90e3e16b8a791d5b9cd13f2c76e1c254aeedebb";
$[0] = "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7";
}
useState(_temp);

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer) @enableResetCacheOnSourceFileChanges
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
import {useEffect, useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableEmitInstrumentForget @compilationMode(annotation) @gating
// @enableEmitInstrumentForget @compilationMode:"annotation" @gating
function Bar(props) {
'use forget';
@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @enableEmitInstrumentForget @compilationMode(annotation) @gating
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @enableEmitInstrumentForget @compilationMode:"annotation" @gating
const Bar = isForgetEnabled_Fixtures()
? function Bar(props) {
"use forget";

View File

@@ -1,4 +1,4 @@
// @enableEmitInstrumentForget @compilationMode(annotation) @gating
// @enableEmitInstrumentForget @compilationMode:"annotation" @gating
function Bar(props) {
'use forget';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
export default function Bar(props) {
'use forget';
return <div>{props.bar}</div>;
@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"annotation"
const Bar = isForgetEnabled_Fixtures()
? function Bar(props) {
"use forget";

View File

@@ -1,4 +1,4 @@
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
export default function Bar(props) {
'use forget';
return <div>{props.bar}</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
export default function Bar(props) {
'use forget';
return <div>{props.bar}</div>;
@@ -35,7 +35,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"annotation"
const Bar = isForgetEnabled_Fixtures()
? function Bar(props) {
"use forget";

View File

@@ -1,4 +1,4 @@
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
export default function Bar(props) {
'use forget';
return <div>{props.bar}</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
export function Bar(props) {
'use forget';
return <div>{props.bar}</div>;
@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"annotation"
export const Bar = isForgetEnabled_Fixtures()
? function Bar(props) {
"use forget";

View File

@@ -1,4 +1,4 @@
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
export function Bar(props) {
'use forget';
return <div>{props.bar}</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
function Bar(props) {
'use forget';
return <div>{props.bar}</div>;
@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"annotation"
const Bar = isForgetEnabled_Fixtures()
? function Bar(props) {
"use forget";

View File

@@ -1,4 +1,4 @@
// @gating @compilationMode(annotation)
// @gating @compilationMode:"annotation"
function Bar(props) {
'use forget';
return <div>{props.bar}</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @gating @compilationMode(infer)
// @gating @compilationMode:"infer"
import React from 'react';
export default React.forwardRef(function notNamedLikeAComponent(props) {
return <div />;
@@ -14,7 +14,7 @@ export default React.forwardRef(function notNamedLikeAComponent(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(infer)
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"infer"
import React from "react";
export default React.forwardRef(
isForgetEnabled_Fixtures()

View File

@@ -1,4 +1,4 @@
// @gating @compilationMode(infer)
// @gating @compilationMode:"infer"
import React from 'react';
export default React.forwardRef(function notNamedLikeAComponent(props) {
return <div />;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @flow @compilationMode(infer)
// @flow @compilationMode:"infer"
export default hook useFoo(bar: number) {
return [bar];
}

View File

@@ -1,4 +1,4 @@
// @flow @compilationMode(infer)
// @flow @compilationMode:"infer"
export default hook useFoo(bar: number) {
return [bar];
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @customMacros(idx.*.b)
// @customMacros:"idx.*.b"
function Component(props) {
// outlined
@@ -31,7 +31,7 @@ function Component(props) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @customMacros(idx.*.b)
import { c as _c } from "react/compiler-runtime"; // @customMacros:"idx.*.b"
function Component(props) {
const $ = _c(16);

View File

@@ -1,4 +1,4 @@
// @customMacros(idx.*.b)
// @customMacros:"idx.*.b"
function Component(props) {
// outlined

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @customMacros(idx.a)
// @customMacros:"idx.a"
function Component(props) {
// outlined
@@ -25,7 +25,7 @@ function Component(props) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @customMacros(idx.a)
import { c as _c } from "react/compiler-runtime"; // @customMacros:"idx.a"
function Component(props) {
const $ = _c(10);

View File

@@ -1,4 +1,4 @@
// @customMacros(idx.a)
// @customMacros:"idx.a"
function Component(props) {
// outlined

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @customMacros(idx)
// @customMacros:"idx"
import idx from 'idx';
function Component(props) {
@@ -21,7 +21,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @customMacros(idx)
import { c as _c } from "react/compiler-runtime"; // @customMacros:"idx"
function Component(props) {
var _ref2;

View File

@@ -1,4 +1,4 @@
// @customMacros(idx)
// @customMacros:"idx"
import idx from 'idx';
function Component(props) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
import {useNoAlias} from 'shared-runtime';
// This should be compiled by Forget
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
import { useNoAlias } from "shared-runtime";
// This should be compiled by Forget

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
import {useNoAlias} from 'shared-runtime';
// This should be compiled by Forget

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
// @inferEffectDependencies @compilationMode:"infer" @panicThreshold:"none"
import useMyEffect from 'useEffectWrapper';
function nonReactFn(arg) {

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
// @inferEffectDependencies @compilationMode:"infer" @panicThreshold:"none"
import useMyEffect from 'useEffectWrapper';
function nonReactFn(arg) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
// @inferEffectDependencies @compilationMode:"infer" @panicThreshold:"none"
import {useEffect} from 'react';
function nonReactFn(arg) {

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
// @inferEffectDependencies @compilationMode:"infer" @panicThreshold:"none"
import {useEffect} from 'react';
function nonReactFn(arg) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
/**

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
/**

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import React from 'react';
function NonReactiveDepInEffect() {

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import React from 'react';
function NonReactiveDepInEffect() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useSpecialEffect} from 'shared-runtime';
/**

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useSpecialEffect} from 'shared-runtime';
/**

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
function Component({propVal}) {

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
function Component({propVal}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
import {print} from 'shared-runtime';
@@ -20,7 +20,7 @@ function Component({foo}) {
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import { useEffect } from "react";
import { print } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
import {print} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
@@ -19,7 +19,7 @@ function Component({arrRef}) {
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import { useEffect, useRef } from "react";
import { print } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
function Component({foo}) {
@@ -17,7 +17,7 @@ function Component({foo}) {
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import { useEffect } from "react";
function Component(t0) {

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @panicThreshold(none)
// @inferEffectDependencies @panicThreshold:"none"
import {useEffect} from 'react';
function Component({foo}) {

View File

@@ -0,0 +1,64 @@
## Input
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr));
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal));
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
function Foo({ propVal }) {
const arr = [propVal];
useEffectWrapper(() => print(arr));
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal));
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"},"suggestions":null,"severity":"InvalidReact"}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":216},"end":{"line":7,"column":36,"index":250},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":245},"end":{"line":7,"column":34,"index":248},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":274},"end":{"line":10,"column":44,"index":316},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":307},"end":{"line":10,"column":42,"index":314},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -0,0 +1,19 @@
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr));
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal));
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};

View File

@@ -0,0 +1,59 @@
## Input
```javascript
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
function Foo({propVal}) {
'use memo';
const arr = [propVal];
useEffectWrapper(() => print(arr));
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal));
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
function Foo({ propVal }) {
"use memo";
const arr = [propVal];
useEffectWrapper(() => print(arr));
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal));
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -0,0 +1,21 @@
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
function Foo({propVal}) {
'use memo';
const arr = [propVal];
useEffectWrapper(() => print(arr));
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal));
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
React.memo(props => {
return <div />;
});
@@ -12,7 +12,7 @@ React.memo(props => {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
React.memo((props) => {
const $ = _c(1);
let t0;

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
React.memo(props => {
return <div />;
});

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode(infer)
// @compilationMode:"infer"
const Component = props => {
return <div />;
};
@@ -12,7 +12,7 @@ const Component = props => {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
const Component = (props) => {
const $ = _c(1);
let t0;

View File

@@ -1,4 +1,4 @@
// @compilationMode(infer)
// @compilationMode:"infer"
const Component = props => {
return <div />;
};

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