Compare commits

...

55 Commits

Author SHA1 Message Date
Lauren Tan
5644e81f27 [eprh] Move to compiler directory
Moves the plugin into the compiler directory.

- Remove eslint-plugin-react-hooks from bundles.js
- Remove eslint-plugin-react-hooks from ReactVersions.js
- Remove jest.config.js
- Remove babel.config-react-compiler.js
- Replace babel.config.js with copy from eslint-plugin-react-compiler
- Add tsup.config.ts to eslint-plugin-react-hooks
- Add eslint-plugin-react-hooks to compiler release scripts
2025-03-19 16:17:24 -04:00
lauren
208905257f [ci] Add cache cleanup workflow (#32675)
> Caches have branch scope restriction in place. This means that if
caches for a specific branch are using a lot of storage quota, it may
result into more frequently used caches from default branch getting
thrashed. For example, if there are many pull requests happening on a
repo and are creating caches, these cannot be used in default branch
scope but will still occupy a lot of space till they get cleaned up by
eviction policy. But sometime we want to clean them up on a faster
cadence so as to ensure default branch is not thrashing.


https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32675).
* __->__ #32675
* #32674
2025-03-19 15:42:04 -04:00
lauren
891a6332e3 [ci] Bump build_and_lint to 25 workers (#32674)
Increases number of workers for `build_and_lint`.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32674).
* #32675
* __->__ #32674
2025-03-19 15:41:48 -04:00
lauren
a4842c92ea [ci] Centralize cache (#32672)
To avoid race conditions where multiple jobs try to write to the same
cache, we now centralize saving the cache and then reusing it in every
subsequent job.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32672).
* #32675
* #32674
* __->__ #32672
2025-03-19 15:25:07 -04:00
Niklas Holm
ada8bbbd6a [eslint-plugin-react-compiler] Fix type error with recommended config (#32666)
## Summary

In the recommended configuration for `eslint-plugin-react-compiler`,
i.e. `reactCompiler.configs.recommended`, the rule is typed as `string`
rather than `eslint.Linter.RuleEntry` or anything assignable thereto,
which results in the following type error if you type check your eslint
configuration:
```
Property ''react-compiler/react-compiler'' is incompatible with index signature.
  Type 'string' is not assignable to type 'RuleEntry | undefined'.
```
Simply adding a const assertion fixes the error.

## How did you test this change?

I emitted declarations for the module and confirmed that the rule is now
typed as the string literal `'error'`
2025-03-19 14:58:08 -04:00
lauren
e9c3b27b4b [ci] Bump all node_modules cache keys (#32671)
I'm seeing a lot of instances of

> Failed to save: Unable to reserve cache with key
runtime-and-compiler-node_modules-v5-X64-Linux-e454609794aae66da9909c77dd6efa073eceff7f44d6527611f8465e102578b4,
another job may be creating this cache.

which is adding ~20 seconds to every step. Let's try to bust the cache
following this
[comment](https://github.com/actions/cache/issues/485#issuecomment-744145040)
and see if that helps.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32671).
* #32672
* __->__ #32671
2025-03-19 14:42:45 -04:00
lauren
b0446ff06a [ci] Properly format commit message take 2 (#32673)
We need to use the commit message from `main`, not the builds branch
2025-03-19 14:01:49 -04:00
lauren
db7dfe0550 [eprh] Don't transpile to es5 (#32669)
Now that we've moved the sync location of the plugin, we no longer need
this since those restrictions no longer apply.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32669).
* __->__ #32669
* #32668
2025-03-19 11:49:42 -04:00
lauren
646835fb59 [ci] Properly format commit message (#32668)
Using the github variable for the commit message replaces the variable
inline. If the commit message contains quotes or other characters that
need to be escaped, this breaks the workflow.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32668).
* #32669
* __->__ #32668
2025-03-19 11:49:18 -04:00
Sebastian Markbåge
c2a1961747 Minor Fixes to View Transition Fixture (#32664)
Follow up to #32656.

Remove touchAction from SwipeRecognizer. I was under the wrong
impression that this was only the touch-action applied to this
particular element, but that parents would still win but in fact this
blocks the parent from scrolling in the other direction. By specifying a
fixed direction it also blocked rage-swiping in the other direction
early on.

Disable pointer-events on view-transition so that the scroll can be hit.
This means that touches hit below the items animating above. This allows
swiping to happen again before momentum scroll has finished. Previously
they were ignored. This only works as long as the SwipeRecognizer is
itself not animating. This means you can now rage-swipe in both
directions quickly.
2025-03-18 19:20:34 -04:00
mofeiZ
6584a6eec4 [compiler] Hoist dependencies from functions more conservatively (#32616)
Alternative to facebook/react#31584 which sets
enableTreatFunctionDepsAsConditional:true` by default.

This PR changes dependency hoisting to be more conservative while trying
to preserve an optimal "happy path". We assume that a function "is
likely called" if we observe the following in the react function body.

- a direct callsite
- passed directly as a jsx attribute or child
- passed directly to a hook
- a direct return

A function is also "likely called" if it is directly called, passed to
jsx / hooks, or returned from another function that "is likely called".

Note that this approach marks the function definition site with its
hoistable properties (not its use site). I tried implementing use-site
hoisting semantics, but it felt both unpredictable (i.e. as a developer,
I can't trust that callbacks are well memoized) and not helpful (type +
null checks of a value are usually colocated with their use site)

In this fixture (copied here for easy reference), it should be safe to
use `a.value` and `b.value` as dependencies, even though these functions
are conditionally called.
```js
// inner-function/nullable-objects/assume-invoked/conditional-call-chain.tsx
function Component({a, b}) {
  const logA = () => {
    console.log(a.value);
  };
  const logB = () => {
    console.log(b.value);
  };
  const hasLogged = useRef(false);
  const log = () => {
    if (!hasLogged.current) {
      logA();
      logB();
      hasLogged.current = true;
    }
  };
  return <Stringify log={log} shouldInvokeFns={true} />;
}
```

On the other hand, this means that we produce invalid output for code
like manually implementing `Array.map`
```js
// inner-function/nullable-objects/bug-invalid-array-map-manual.js
function useFoo({arr1, arr2}) {
  const cb = e => arr2[0].value + e.value;
  const y = [];
  for (let i = 0; i < arr1.length; i++) {
    y.push(cb(arr1[i]));
  }
  return y;
}
```
2025-03-18 18:00:08 -04:00
dan
86d5ac0882 Revert "Fix:- Improve HOC support and state preservation in React Refresh" (#32214)
Reverts facebook/react#30660

I don’t feel confident in the approach. This part of code is supposed to
rely on the module bundler behaving as expected. _Maybe_ this is correct
but I need to review it closer — it was intentionally _not_ implemented
this way originally.

I’ll try to take a closer look some time this week. We don’t have to
merge this revert right now but just flagging that I don’t understand
the thinking behind the new approach and don’t have confidence in it.
2025-03-18 19:05:56 +00:00
Jack Pope
476f53879e Add getClientRects to fragment instances (#32660)
Adds `getClientRects()` to fragment instances with a fixture test case.
`Element.getClientRect` returns a collection of `DOMRect`s (see example
of multiline span returning two `DOMRect` boxes).
`fragmentInstance.getClientRects` here flattens those collections into
an array of rects.
2025-03-18 13:54:26 -04:00
Jack Pope
c69a5fc53a Add blur() and focusLast() to fragment instances (#32654)
`focus()` was added in https://github.com/facebook/react/pull/32465.
Here we add `focusLast()` and `blur()`. I also extended `focus` to take
options.

`focus` will focus the first focusable element. `focusLast` will focus
the last focusable element. We could consider a `focusFirst` naming or
even the `focusWithin` used by test selector APIs as well.

`blur` will only have an effect if the current `document.activeElement`
is one of the fragment children.
2025-03-18 11:58:12 -04:00
Sebastian Markbåge
a35aaf704c Update ViewTransition fixture to include bigger buttons/swipe (#32656)
I made the button a bit bigger and moved the swipe recognizer around the
whole screen. Typically these are used around the whole content without
any affordances and not as a standalone scrubber. Ideally the swipe
would be able to be inside the animating content but it can't yet due to
[this Safari bug](https://bugs.webkit.org/show_bug.cgi?id=288795).

Added back some paragraphs so that scrolling can be tested properly. It
appears it's possible to get the swipe to be a bit misaligned if you
scroll enough on iOS.

<img width="437" alt="Screenshot 2025-03-17 at 10 27 42 PM"
src="https://github.com/user-attachments/assets/589dc828-717e-420c-83dc-94ae6ad59791"
/>
2025-03-18 09:24:31 -04:00
Sebastian Markbåge
3c3696d554 Measure Updated ViewTransition Boundaries (#32653)
This does the same thing for `measureUpdateViewTransition` that we did
for `measureNestedViewTransitions` in
e3cbaffef0.
If a boundary hasn't mutated and didn't change in size, we mark it for
cancellation. Otherwise we add names to it. The different from the
CommitViewTransition path is that the "old" names are added to the
clones so this is the first time the "new" names.

Now we also cancel any boundaries that were unchanged. So now the root
no longer animates. We still have to clone them. There are other
optimizations that can avoid cloning but once we've done all the layouts
we can still cancel the running animation and let them just be the
regular content if they didn't change. Just like the regular
fire-and-forget path.

This also fixes the measurement so that we measure clones by adjusting
their position back into the viewport.

This actually surfaces a bug in Safari that was already in #32612. It
turns out that the old names aren't picked up for some reason and so in
Safari they looked more like a cross-fade than what #32612 was supposed
to fix. However, now that bug is even more apparent because they
actually just disappear in Safari. I'm not sure what that bug is but
it's unrelated to this PR so will fix that separately.
2025-03-17 21:38:13 -04:00
Dimitri POSTOLOV
90b511ec7a fix(react-compiler): implement NumericLiteral as ObjectPropertyKey (#31791) 2025-03-17 19:30:58 -04:00
Sebastian Markbåge
02372952e4 Don't auto-start browser in SSR fixtures (#32652)
I end up restarting these a lot and it's annoying to have it open
another tab each time.

The flight fixture already doesn't auto-start.
2025-03-17 17:26:00 -04:00
Sebastian Markbåge
9fde224a53 Materialize the tree ID when ViewTransition name=auto consumes one (#32651)
ViewTransition uses the `useId` algorithm to auto-assign names. This
ensures that we could animate between SSR content and client content by
ensuring that the names line up.

However, I missed that we need to bump the id (materialize it) when we
do that. This is what function components do if they use one or more
`useId()`. This caused duplicate names when two ViewTransitions were
nested without any siblings since they would share name.
2025-03-17 16:17:01 -04:00
lauren
ca02c4bb40 [ci][ez] use yarn --cwd (#32650)
Run yarn install via `--cwd` instead of `working-directory` to make the
labels clearer
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32650).
* __->__ #32650
* #32649
* #32648
2025-03-17 14:13:27 -04:00
lauren
c37d89827e [ci] Pin Discord webhook action to specific commit sha (#32649)
Pins the discord webhook action to
`86dc739f3f165f16dadc5666051c367efa1692f4`, which is what the v6 tag
points to.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32649).
* #32650
* __->__ #32649
* #32648
2025-03-17 14:13:17 -04:00
lauren
6c6699f3d2 [ci] Don't use third party action to push commits (#32648)
In light of recent third party actions being compromised, let's just
push the commit ourselves rather than use a third party action. We
already detect if changes are needed, so the step will only run if so.

I also added a `dry_run` option to the manual runs of this workflow for
testing.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32648).
* #32650
* #32649
* __->__ #32648
2025-03-17 14:13:02 -04:00
Andrew Clark
9320a0139d Fix COMMIT_SHA when generating PR artifacts (#32647)
Follow-up to #31850. We want to build using the original commit SHA, not
the merge commit that GitHub Actions creates behind the scenes. We were
already checking out the correct commit object, but the COMMIT_SHA
artifact was still pointing to the merge commit.

This should fix the sizebot links to point to working URLs, too.
2025-03-17 12:46:27 -04:00
Ricky
fbcda19a23 [devtools] add filters for internal builds (#32646)
We don't have an experimental-only build of devtools, but we can at
least add these filters to the internal build.

A better way would be to use feature detection, but I'm not sure how and
this isn't a very heavily used feautre.
2025-03-17 12:14:19 -04:00
Jack Pope
cd28a946d5 Add observer methods to fragment instances (#32619)
This implements `observeUsing(observer)` and `unobserverUsing(observer)`
on fragment instances. IntersectionObservers and ResizeObservers can be
passed to observe each host child of the fragment. This is the
equivalent to calling `observer.observe(child)` or
`observer.unobserve(child)` for each child target.

Just like the addEventListener, the observer is held on the fragment
instance and applied to any newly mounted child. So you can do things
like wrap a paginated list in a fragment and have each child
automatically observed as they commit in.

Unlike, the event listeners though, we don't `unobserve` when a child is
removed. If a removed child is currently intersecting, the observer
callback will be called when it is removed with an empty rect. This lets
you track all the currently intersecting elements by setting state from
the observer callback and either adding or removing them from your list
depending on the intersecting state. If you want to track the removal of
items offscreen, you'd have to maintain that state separately and append
intersecting data to it in the observer callback. This is what the
fixture example does.

There could be more convenient ways of managing the state of multiple
child intersections, but basic examples are able to be modeled with the
simple implementation. Let's see how the usage goes as we integrate this
with more advanced loggers and other features.

For now you can only attach one observer to an instance. This could
change based on usage but the fragments are composable and could be
stacked as one way to apply multiple observers to the same elements.

In practice, one pattern we expect to enable is more composable logging
such as

```javascript
function Feed({ items }) {
  return (
    <ImpressionLogger>
      {items.map((item) => (
        <FeedItem />
      ))}
    </ImpressionLogger>
  );
}
```

where `ImpressionLogger` would set up the IntersectionObserver using a
fragment ref with the required business logic and various components
could layer it wherever the logging is needed. Currently most callsites
use a hook form, which can require wiring up refs through the tree and
merging refs for multiple loggers.
2025-03-17 11:40:05 -04:00
Ricky
8243f3f063 [bug] Fix component name for Portal and add tests (#32640)
Based off: https://github.com/facebook/react/pull/32499

While looking into `React.lazy` issues for built-ins, I noticed we
already error for `lazy` with build-ins, but we don't have any tests for
`getComponentNameFromType` using all the built-ins. This may be
something we should handle, but for now we should at least have tests.

Here's why: while writing tests, I noticed we check `type` instead of
`$$typeof` for portals:


9cdf8a99ed/packages/react-reconciler/src/ReactPortal.js (L25-L32)

This PR adds tests for all the built-ins and fixes the portal bug.

[Commit to
review](e068c167d4)
2025-03-17 11:23:28 -04:00
Ricky
df31952275 Remove offscreen type (#32639)
Based off https://github.com/facebook/react/pull/32499

This is no longer used.

[Review
commit](88c297d12f)
2025-03-17 09:37:07 -04:00
Ricky
1a191701fe [refactor] Add element type for Activity (#32499)
This PR separates Activity to it's own element type separate from
Offscreen. The goal is to allow us to add Activity element boundary
semantics during hydration similar to Suspense semantics, without
impacting the Offscreen behavior in suspended children.
2025-03-17 09:17:00 -04:00
Ricky
99563e9173 Partially revert #32588 (#32621)
https://github.com/facebook/react/pull/32588 changed the babel config
impacting local tests, and I'm not able to run test:

<img width="1354" alt="Screenshot 2025-03-15 at 2 37 00 PM"
src="https://github.com/user-attachments/assets/2d4afe39-6ab6-4c83-87a9-ceb0ee5f8df5"
/>


This PR reverts those changes until we can re-land with a fix.
2025-03-15 15:21:57 -04:00
Sebastian Markbåge
17d274dc12 Remove Mutation Check Around commit/measureUpdateViewTransition (#32617)
There's two ways to find updated View Transitions.

One is the "commit/measureNestedViewTransitions" pass which is used to
find things in unchanged subtrees. This can only lead to the relayout
case since there's can't possibly be any mutations in the subtree. This
is only triggered when none of the direct siblings have any mutations at
all.

The other case is "commit/measureUpdateViewTransition" which is for a
ViewTransition that itself has mutations scheduled inside of it which
leads to the "update" case.

However, there's a case between these two cases. When a direct sibling
has a mutation but there's also a ViewTransition exactly at the same
level. In that case we can't bail out on the whole set of children so we
won't trigger the "nested" case. Previously we also didn't trigger the
"commit/measureUpdateViewTransition" case because we first checked if
that had any mutations inside of it at all. This leads to neither case
picking up this boundary.

We could check if the ViewTransition itself has any mutations inside and
if not trigger the nested path.

There's a simpler way though. Because
`commit/measureUpdateViewTransition` is actually just optimistic. The
flags are pessimistic and we don't know for sure if there will actually
be a mutation until we've traversed the tree. It can sometimes lead to
the "relayout" case. So we can just use that same path, knowing that
it'll just lead to the layout pass. Therefore it's safe to just remove
this check.
2025-03-14 17:38:53 -04:00
Sebastian Markbåge
6b5d9fd316 Move traverseFragmentInstanceChildren to internal ReactFiberTreeReflection (#32613)
This is a nit but a Config should not have to know anything about the
internals of Fibers. Ideally it shouldn't even access them but we have
some cases where we need pointers back in like for this fragment.

The way we've typically abstracted this is using the
`ReactFiberTreeReflection` helper that's in the `react-reconciler`. Such
as in the event system.
f3c956006a/packages/react-dom-bindings/src/events/ReactDOMEventListener.js (L22-L26)

We sometimes cheat but we really should clean this up such that a
`Fiber` is actually an opaque type to the Configs and it can never dot
into it without using a helper.

So this just moves `traverseFragmentInstanceChildren` to
ReactFiberTreeReflection so that the ConfigDOM doesn't ever dot into its
fields itself. It just passes the Fiber through back into the
react-reconciler. I had to add a wrapper to read the `.child` to avoid
that being assumed too. I also noticed that FragmentInstanceType is not
actually passed through so that argument is unnecessary.
2025-03-14 17:38:35 -04:00
Sebastian Markbåge
2c560374d6 Measure and apply names for the "new" phase (#32612)
Stacked on #32599 and #32611.

This is able to reuse the code from CommitViewTransitions for "enter",
"shared" and "layout". The difference is that for "enter"/"shared" in
the "new" phase we pass in the deletions.

For "layout" of nested boundaries we just need to measure the clones at
the same time we measure the original nodes since we haven't measured
them in a previous phase in the current approach.

With these updates, things move around more like expected in the fixture
because we're now applying the appropriate pairs to trigger individual
animations instead of just the full document cross-fade.

The "update" phase is a little more complicated and is coming soon.
2025-03-14 14:26:55 -04:00
Sebastian Markbåge
2e385738a4 Find Pairs and Apply View Transition Names to the Clones in the "old" Phase (#32599)
Stacked on #32578.

We need to apply view-transition-names to the clones that we create in
the "old" phase for the ViewTransition boundaries that should activate.

Finding pairs is a little trickier than in
ReactFiberCommitViewTransitions. Normally we collect all name
"insertions" in the `accumulateSuspenseyCommit` phase before we even
commit. Then in the snapshot do we visit all "deletions" and since we
already collected all the insertions we know immediately if the deletion
had a pair and should therefore get a "name" assigned to activate the
boundary. For ReactFiberApplyGesture we need to assign names to
"insertions" since it's in reverse but we don't already have a map of
deletions. Therefore we need to first visit all deletions.

Instead of doing that in a completely separate pass, we instead visit
deletions in the same pass to find pairs. Since this is in the same pass
we might visit insertions before deletions or vice versa depending on
document order. However, we can deal with this by applying the name to
the insertion when we find the deletion if we've already made the clones
at that point.

Applying names to pure exits, updates or nested (relayout) is a bit more
straight-forward.
2025-03-14 14:15:39 -04:00
Sebastian Markbåge
c4a3b92e09 Add more phases to the ReactFiberApplyGesture (#32578)
Stacked on #32585 and #32605.

This adds more loops for the phases of "Apply Gesture". It doesn't
implement the interesting bit yet like adding view-transition-names and
measurements. I'll do that in a separate PR to keep reviewing easier.

The three phases of this approach is roughly:

- Clone and apply names to the "old" state.
- Inside startViewTransition: Apply names to the "new" state. Measure
both the "old" and "new" state to know whether to cancel some of them.
Delete the clones which will include all the "old" names.
- After startViewTransition: Restore "new" names back to no
view-transition-name.

Since we don't have any other Effects in these phases we have a bit more
flexibility and we can avoid extra phases that traverse the tree. I've
tried to avoid any additional passes.

An interesting consequence of this approach is that we could measure
both the "old" and "new" state before `startViewTransition`. This would
be more efficient because we wouldn't need to take View Transition
snapshots of parts of the tree that won't actually animate. However,
that would require an extra pass and force layout earlier. It would also
have different semantics from the fire-and-forget View Transitions
because we could optimize better which can be visible. It would also not
account for any late mutations. So I decided to instead let the layout
be computed by painting as usual and then measure both "old" and "new"
inside the startViewTransition instead. Then canceling anything that
doesn't animate to keep it consistent.

Unfortunately, though there's not a lot of code sharing possible in
these phases because the strategy is so different with the cloning and
because the animation is performed in reverse. The "finishedWork" Fiber
represents the "old" state and the "current" Fiber represents the "new"
state.

The most complicated phase is the cloning. I actually ended up having to
make a very different pattern from the other phases and CommitWork in
general. Because we have to clone as we go and also do other things like
apply names and finding pairs, it has more phases. I ended up with an
approach that uses three different loops. The outer one for updated
trees, one for inserted trees that don't need cloning (doesn't include
reappearing offscreen) and one for not updated trees that still need
cloning. Inside each loop it can also be in different phases which I
track with the `visitPhase` enum - this pattern is kind of new.

Additionally, we need to measure the cloned nodes after we've applied
mutations to them and we have to wait until the whole tree is inserted.
We don't have a reference to these DOM elements in the Fiber tree since
that still refers to the original ones. We need to store the cloned
elements somewhere. So I added a temporary field on the
ViewTransitionState to keep track of any clones owned by that
ViewTransition.

When we deep clone an unchanged subtree we don't have DOM element
instances. It wouldn't be quite safe to try to find them from the tree
structure. So we need to avoid the deep clones if we might need DOM
elements. Therefore we keep traversing in the case where we need to find
nested ViewTransition boundaries that are either potentially affected by
layout or a "pair".

For the other two phases the pattern there's a lot of code duplication
since it's slightly different from the commit ones but they at least
follow the same pattern. For the restore phase I was actually able to
reuse most of the code.

I don't love how much code this is.
2025-03-14 13:20:17 -04:00
Sebastian Markbåge
6daef4e7c8 Make xViewTransitionToHostInstances helpers reusable (#32611)
This prepares from being able to reuse some this in ApplyGesture.

These all start with resetting a counter but it's tricky to have to
remember to do this and tricky to do from the outside of this module. So
we make an exported helper that does the resetting. Ideally it gets
inlined.

We also stop passing "current" to measureViewTransitionHostInstances.
Same thing for cancelViewTransitionHostInstances. This doesn't make
sense for "nested" which has not updated and so might not have an
alternate. Instead we pass in the old and new name if they might be
different.
2025-03-14 13:16:42 -04:00
Sebastian Markbåge
3e956805e8 Gate flushGestureMutations and flushGestureAnimations (#32605)
Normally these are gated by the whole commitGestureOnRoot path but in
the case of an early commit these phases may need to be invoked.
Earlier. Those paths weren't gated which I noticed when I started adding
code to them.
2025-03-14 13:16:30 -04:00
Sebastian Markbåge
1b6e3dd985 Merge restoreEnterViewTransitions and restoreExitViewTransitions (#32585)
This is the exact same code in both cases. It's just general clean up.

By unifying them it becomes less confusing to reuse these helpers in the
Apply Gesture path where the naming is reversed.
2025-03-14 13:16:20 -04:00
mofeiZ
5398b71158 [compiler] detect and throw on untransformed required features (#32512)
Traverse program after running compiler transform to find untransformed
references to compiler features (e.g. `inferEffectDeps`, `fire`).

Hard error to fail the babel pipeline when the compiler fails to
transform these features to give predictable runtime semantics.
Untransformed calls to functions like `fire` will throw at runtime
anyways, so let's fail the build to catch these earlier.

Note that with this fails the build *regardless of panicThreshold*
2025-03-14 11:44:49 -04:00
lauren
f3c956006a [ci] Update node_modules cache path (#32609)
Alternative of #32604.

Bust all old caches since I reverted some other changes to the keys.
2025-03-13 22:29:48 -04:00
lauren
4eba294c69 [ci] Cache runtime and compiler only for test runs (#32608)
We only need the compiler built for `yarn test` in the root directory.
Rather than always cache both for every step, let's just do it where
it's needed explicitly.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32608).
* #32609
* __->__ #32608
2025-03-13 22:21:35 -04:00
lauren
5200721e5c Revert "[ci] Update yarn and node_modules cache key (#32603)" (#32607)
This reverts commit 67338703aa.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32607).
* #32609
* #32608
* __->__ #32607
2025-03-13 22:14:00 -04:00
lauren
e9d80d939e Revert "[ci] Fix node_modules cache glob (#32604)" (#32606)
This reverts commit ef1103d3e9.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32606).
* #32609
* #32608
* #32607
* __->__ #32606
2025-03-13 21:52:46 -04:00
lauren
ef1103d3e9 [ci] Fix node_modules cache glob (#32604)
Seems like the stringified cache path can cause some directories not to
be cached, trying an alternative format
2025-03-13 20:59:27 -04:00
lauren
67338703aa [ci] Update yarn and node_modules cache key (#32603)
Now that the compiler lint rule is merged into
eslint-plugin-react-hooks, we also need to update our caches so compiler
dependencies are also cached. This should fix the CI walltime regression
we are now seeing.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32603).
* #32604
* __->__ #32603
2025-03-13 20:31:04 -04:00
mofeiZ
7939d92fcc [compiler] clean up retry pipeline: fireRetry flag -> compileMode (#32511)
Removes `EnvironmentConfig.enableMinimalTransformsForRetry` in favor of
`run` parameters. This is a minimal difference but lets us explicitly
opt out certain compiler passes based on mode parameters, instead of
environment configurations

Retry flags don't really make sense to have in `EnvironmentConfig`
anyways as the config is user-facing API, while retrying is a compiler
implementation detail.

(per @josephsavona's feedback
https://github.com/facebook/react/pull/32164#issuecomment-2608616479)
> Re the "hacky" framing of this in the PR title: I think this is fine.
I can see having something like a compilation or output mode that we use
when running the pipeline. Rather than changing environment settings
when we re-run, various passes could take effect based on the
combination of the mode + env flags. The modes might be:
>
> * Full: transform, validate, memoize. This is the default today.
> * Transform: Along the lines of the backup mode in this PR. Only
applies transforms that do not require following the rules of React,
like `fire()`.
> * Validate: This could be used for ESLint.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32511).
* #32512
* __->__ #32511
2025-03-13 19:54:54 -04:00
mofeiZ
d92e5713be [compiler] Avoid bailouts when inserting gating (#32598)
This change fixes a coverage hole in rolling out with `gating`. Prior to
this PR, configuring `gating` causes React Compiler to bail out of
optimizing some functions.

This means that it's not entirely safe to cutover from `gating` enabled
for all users (i.e. rolled out 100%) to removing the `gating` config
altogether, as new functions may be opted into compilation when they
stop bailing out due to gating-specific logic.

This is technically slightly slower due to the additional function
indirection. An alternative approach is to recommend running a codemod
to insert `use no memo`s on currently-bailing out functions before
removing the`gating` config.

---
Tested [internally](
https://fburl.com/diff/q982ovua) by enabling on a page that previously
had a few hundred bailouts due to gating + hoisted function declarations
and (1) clicking around locally and (2) running a bunch of e2e tests
2025-03-13 19:31:49 -04:00
mofeiZ
93b61fc4ec [compiler][ez] Stop bailing out early for hoisted gated functions (#32597)
Some code movement for the next PR
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32597).
* #32598
* __->__ #32597
2025-03-13 19:08:38 -04:00
lauren
77987e5ee3 [ci] mkdir before mv (#32602)
Missed this earlier.
2025-03-13 17:46:46 -04:00
lauren
0df46f01a9 [ci] Update eslint-plugin-react-hooks output location for Meta builds (#32601)
Updates where this file is output so we can sync it independently to
another directory.
2025-03-13 16:54:39 -04:00
mofeiZ
f457d0b4c6 [compiler][ez] Only fail gating hoisting check for referenced identifiers (#32596)
Reduce false positive bailouts by using the same
`isReferencedIdentifier` logic that the compiler also uses for
determining context variables and a function's own hoisted declarations.

Details:
Previously, we counted every babel identifier as a reference. This is
problematic because babel counts most string symbols as an identifier.

```js
print(x);  // x is an identifier as expected
obj.x      // x is.. also an identifier here
{x: 2}     // x is also an identifier here
```

This PR adds a check for `isReferencedIdentifier`. Note that only
non-lval
references pass this check. This should be fine as we don't need to
hoist function declarations before writes to the same lvalue (which
should error in strict mode anyways)
```js
print(x);  // isReferencedIdentifier(x) -> true
obj.x      // isReferencedIdentifier(x) -> false
{x: 2}     // isReferencedIdentifier(x) -> false
x = 2      // isReferencedIdentifier(x) -> false
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32596).
* __->__ #32596
* #32595
* #32594
* #32593
* #32522
* #32521
2025-03-13 12:10:22 -04:00
mofeiZ
1c79cb82ab [compiler][ez] Move compiler gating tests (#32595)
Move all gating tests to `gating/`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32595).
* #32596
* __->__ #32595
* #32594
* #32593
* #32522
* #32521
2025-03-13 12:06:48 -04:00
mofeiZ
89a46a57df [compiler][optim] more shapes for mixedreadonly (#32594)
- Add `at`, `indexOf`, and `includes`
- Optimize MixedReadOnly which is currently only used by hook return
values. Hook return values are typed as Frozen, this change propagates
that to return values of aliasing function calls (such as `at`). One
potential issue is that developers may pass
`enableAssumeHooksFollowRulesOfReact:false` and set
`transitiveMixedData`, expecting their transitive mixed data to be
mutable. This is a bit of an edge case and already doesn't have clear
semantics.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32594).
* #32596
* #32595
* __->__ #32594
* #32593
* #32522
* #32521
2025-03-13 11:59:50 -04:00
mofeiZ
eb53139ee5 [compiler][optim] infer mixedReadOnly for numeric and computed properties (#32593)
Expand type inference to infer mixedReadOnly types for numeric and
computed property accesses.
```js
function Component({idx})
  const data = useFragment(...)
  // we want to type `posts` correctly as Array
  const posts = data.viewers[idx].posts.slice(0, 5);
  // ...
}
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32593).
* #32596
* #32595
* #32594
* __->__ #32593
* #32522
* #32521
2025-03-13 11:58:40 -04:00
mofeiZ
38a7600920 [compiler][optim] Add shape for Array.from (#32522)
(see title)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32522).
* #32596
* #32595
* #32594
* #32593
* __->__ #32522
* #32521
2025-03-13 11:58:17 -04:00
mofeiZ
ed1264f077 [compiler] Patch array and argument spread mutability (#32521)
Array and argument spreads may mutate stateful iterables. Spread sites
should have `ConditionallyMutate` effects (e.g. mutate if the ValueKind
is mutable, otherwise read).

See
- [ecma spec (13.2.4.1 Runtime Semantics: ArrayAccumulation.
SpreadElement : ...
AssignmentExpression)](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-arrayaccumulation).
- [ecma spec 13.3.8.1 Runtime Semantics:
ArgumentListEvaluation](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-argumentlistevaluation)

Note that
- Object and JSX Attribute spreads do not evaluate iterables (srcs
[mozilla](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#description),
[ecma](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-propertydefinitionevaluation))
- An ideal mutability inference system could model known collections
(i.e. Arrays or Sets) as a "mutated collection of non-mutable objects"
(see `todo-granular-iterator-semantics`), but this is not what we do
today. As such, an array / argument spread will always extend the range
of built-in arrays, sets, etc
- Due to HIR limitations, call expressions with argument spreads may
cause unnecessary bailouts and/or scope merging when we know the call
itself has `freeze`, `capture`, or `read` semantics (e.g.
`useHook(...mutableValue)`)
We can deal with this by rewriting these call instructions to (1) create
an intermediate array to consume the iterator and (2) capture and spread
the array at the callsite
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32521).
* #32596
* #32595
* #32594
* #32593
* #32522
* __->__ #32521
2025-03-13 11:58:02 -04:00
278 changed files with 9542 additions and 2105 deletions

View File

@@ -336,11 +336,11 @@ module.exports = {
'packages/react-devtools-extensions/**/*.js',
'packages/react-devtools-timeline/**/*.js',
'packages/react-native-renderer/**/*.js',
'packages/eslint-plugin-react-hooks/**/*.js',
'packages/jest-react/**/*.js',
'packages/internal-test-utils/**/*.js',
'packages/**/__tests__/*.js',
'packages/**/npm/*.js',
'compiler/packages/eslint-plugin-react-hooks/**/*.js',
],
rules: {
'react-internal/prod-error-codes': OFF,
@@ -515,7 +515,7 @@ module.exports = {
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
files: ['compiler/packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin'],

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v6.0.0
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.COMPILER_DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}

View File

@@ -36,8 +36,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: npx playwright install --with-deps chromium
- run: CI=true yarn test

View File

@@ -46,8 +46,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Publish packages to npm
run: |

View File

@@ -46,8 +46,9 @@ jobs:
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn workspace babel-plugin-react-compiler lint
@@ -66,8 +67,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn workspace babel-plugin-react-compiler jest
@@ -90,8 +92,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: xvfb-run -a yarn workspace ${{ matrix.workspace_name }} test
if: runner.os == 'Linux' && matrix.workspace_name == 'react-forgive'

View File

@@ -29,13 +29,13 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- run: yarn --cwd scripts/release install --frozen-lockfile
- name: Download react-devtools artifacts for base revision
run: |
git fetch origin main
@@ -63,8 +63,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -120,8 +121,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4
@@ -154,8 +156,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4

View File

@@ -17,6 +17,75 @@ env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
jobs:
# ----- NODE_MODULES CACHE -----
# Centralize the node_modules cache so it is saved once and each subsequent job only needs to
# restore the cache. Prevents race conditions where multiple workflows try to write to the cache.
runtime_node_modules_cache:
name: Cache Runtime node_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
if: steps.node_modules.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
if: steps.node_modules.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
name: Discover flow inline configs
@@ -36,7 +105,7 @@ jobs:
flow:
name: Flow check ${{ matrix.flow_inline_config_shortname }}
needs: discover_flow_inline_configs
needs: [discover_flow_inline_configs, runtime_node_modules_cache]
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -52,19 +121,22 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }}
# ----- FIZZ -----
check_generated_fizz_runtime:
name: Confirm generated inline Fizz runtime is up to date
needs: [runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -76,14 +148,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
@@ -91,6 +165,7 @@ jobs:
# ----- FEATURE FLAGS -----
flags:
name: Check flags
needs: [runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -102,19 +177,22 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn flags
# ----- TESTS -----
test:
name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard }})
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -153,27 +231,34 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# yml is dumb. update the --total arg to yarn build if you change the number of workers
worker_id: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
worker_id: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]
release_channel: [stable, experimental]
steps:
- uses: actions/checkout@v4
@@ -183,21 +268,27 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 11.0.22
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn build --index=${{ matrix.worker_id }} --total=25 --r=${{ matrix.release_channel }} --ci
env:
CI: github
RELEASE_CHANNEL: ${{ matrix.release_channel }}
@@ -214,7 +305,7 @@ jobs:
test_build:
name: yarn test-build
needs: build_and_lint
needs: [build_and_lint, runtime_compiler_node_modules_cache]
strategy:
fail-fast: false
matrix:
@@ -261,16 +352,22 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -283,7 +380,7 @@ jobs:
process_artifacts_combined:
name: Process artifacts combined
needs: build_and_lint
needs: [build_and_lint, runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -295,14 +392,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -311,7 +410,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
@@ -330,7 +429,7 @@ jobs:
check_error_codes:
name: Search build artifacts for unminified errors
needs: build_and_lint
needs: [build_and_lint, runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -342,14 +441,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -365,7 +466,7 @@ jobs:
check_release_dependencies:
name: Check release dependencies
needs: build_and_lint
needs: [build_and_lint, runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -377,14 +478,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -409,15 +512,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
id: node_modules
with:
path: "**/node_modules"
key: fixtures_dom-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: fixtures_dom-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
working-directory: fixtures/dom
- run: yarn --cwd fixtures/dom install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -452,14 +556,16 @@ jobs:
# That means dependencies of the built packages are not installed.
# We need to install dependencies of the workroot to fulfill all dependency constraints
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
id: node_modules
with:
path: "**/node_modules"
key: fixtures_flight-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: fixtures_flight-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -498,7 +604,7 @@ jobs:
# ----- DEVTOOLS -----
build_devtools_and_process_artifacts:
name: Build DevTools and process artifacts
needs: build_and_lint
needs: [build_and_lint, runtime_node_modules_cache]
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -514,14 +620,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -553,7 +661,7 @@ jobs:
run_devtools_e2e_tests:
name: Run DevTools e2e tests
needs: build_and_lint
needs: [build_and_lint, runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -565,14 +673,16 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -590,7 +700,7 @@ jobs:
sizebot:
if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' && github.event.pull_request.base.ref == 'main' }}
name: Run sizebot
needs: [build_and_lint]
needs: [build_and_lint, runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -602,15 +712,18 @@ jobs:
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
uses: actions/cache/restore@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
working-directory: scripts/release
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Download artifacts for base revision
run: |
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=$(git rev-parse ${{ github.event.pull_request.base.sha }})
@@ -626,6 +739,7 @@ jobs:
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build for PR
uses: actions/download-artifact@v4
with:
@@ -638,7 +752,7 @@ jobs:
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- run: echo ${{ github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: node ./scripts/tasks/danger
- name: Archive sizebot results
uses: actions/upload-artifact@v4

View File

@@ -16,6 +16,11 @@ on:
required: true
default: false
type: boolean
dry_run:
description: Perform a dry run (run everything except push)
required: true
default: false
type: boolean
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -73,15 +78,13 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
name: yarn install (react)
- run: yarn install --frozen-lockfile
name: yarn install (scripts/release)
working-directory: scripts/release
- run: yarn --cwd scripts/release install --frozen-lockfile
- name: Download artifacts for base revision
run: |
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
@@ -107,9 +110,10 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Move eslint-plugin-react-hooks into facebook-www
# Move eslint-plugin-react-hooks into eslint-plugin-react-hooks
mkdir ./compiled/eslint-plugin-react-hooks
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
./compiled/facebook-www/eslint-plugin-react-hooks.js
./compiled/eslint-plugin-react-hooks/index.js
# Move unstable_server-external-runtime.js into facebook-www
mv build/oss-experimental/react-dom/unstable_server-external-runtime.js \
@@ -242,18 +246,22 @@ jobs:
run: |
echo ":"
git status -u
- name: Check commit message
if: inputs.dry_run
run: |
git fetch origin --quiet
git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:"%B"
- name: Commit changes to branch
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
run: |
git config --global user.email "${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}"
git config --global user.name "${{ github.triggering_actor }}"
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
branch: builds/facebook-www
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
create_branch: true
git fetch origin --quiet
git commit -m "$(git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:'%B%n%nDiffTrain build for [${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha}})')" || echo "No changes to commit"
- name: Push changes to branch
if: inputs.dry_run == false && (inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true')
run: git push
commit_fbsource_artifacts:
needs: download_artifacts
@@ -409,15 +417,19 @@ jobs:
run: |
git add .
git status
- name: Check commit message
if: inputs.dry_run
run: |
git fetch origin --quiet
git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:"%B"
- name: Commit changes to branch
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
run: |
git config --global user.email "${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}"
git config --global user.name "${{ github.triggering_actor }}"
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
branch: builds/facebook-fbsource
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
create_branch: true
git fetch origin --quiet
git commit -m "$(git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:'%B%n%nDiffTrain build for [${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha}})')" || echo "No changes to commit"
- name: Push changes to branch
if: inputs.dry_run == false && (inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true')
run: git push

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v6.0.0
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}

View File

@@ -35,16 +35,20 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "node_modules"
key: runtime-eslint_e2e-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: |
**/node_modules
key: runtime-and-compiler-eslint_e2e-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn --cwd compiler install --frozen-lockfile
- name: Build plugin
working-directory: fixtures/eslint-v${{ matrix.eslint_major }}
run: node build.mjs

View File

@@ -39,13 +39,13 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- run: yarn --cwd scripts/release install --frozen-lockfile
- run: |
scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
cp ./scripts/release/ci-npmrc ~/.npmrc

View File

@@ -44,7 +44,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v6.0.0
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.sender.login }}
@@ -71,13 +71,13 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- run: yarn --cwd scripts/release install --frozen-lockfile
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- if: '${{ inputs.only_packages }}'
name: 'Prepare ${{ inputs.only_packages }} from NPM'

View File

@@ -0,0 +1,35 @@
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
name: (Shared) Cleanup Branch Caches
on:
pull_request:
types:
- closed
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
# `actions:write` permission is required to delete caches
# See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
actions: write
contents: read
steps:
- name: Cleanup
run: |
echo "Fetching list of cache key"
cacheKeysForPR=$(gh cache list --ref $BRANCH --limit 100 --json id --jq '.[].id')
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh cache delete $cacheKey
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge

View File

@@ -28,8 +28,9 @@ jobs:
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -48,8 +49,9 @@ jobs:
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -68,8 +70,9 @@ jobs:
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -88,8 +91,9 @@ jobs:
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile

View File

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

View File

@@ -1,19 +0,0 @@
'use strict';
/**
* HACK: @poteto React Compiler inlines Zod in its build artifact. Zod spreads values passed to .map
* which causes issues in @babel/plugin-transform-spread in loose mode, as it will result in
* {undefined: undefined} which fails to parse.
*
* [@babel/plugin-transform-block-scoping', {throwIfClosureRequired: true}] also causes issues with
* the built version of the compiler. The minimal set of plugins needed for this file is reexported
* from babel.config-ts.
*
* I will remove this hack later when we move eslint-plugin-react-hooks into the compiler directory.
**/
const baseConfig = require('./babel.config-ts');
module.exports = {
plugins: baseConfig.plugins,
};

View File

@@ -4,8 +4,7 @@ module.exports = {
plugins: [
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
'@babel/plugin-transform-classes',
['@babel/plugin-proposal-class-properties', {loose: true}],
'syntax-trailing-function-commas',
[
'@babel/plugin-proposal-object-rest-spread',

View File

@@ -16,7 +16,7 @@
"snap:build": "yarn workspace snap run build",
"snap:ci": "yarn snap:build && yarn snap",
"ts:analyze-trace": "scripts/ts-analyze-trace.sh",
"lint": "yarn eslint src",
"lint": "../../node_modules/eslint-v8/bin/eslint.js src",
"watch": "yarn build --watch"
},
"dependencies": {
@@ -43,7 +43,7 @@
"babel-jest": "^29.0.3",
"babel-plugin-fbt": "^1.0.0",
"babel-plugin-fbt-runtime": "^1.0.0",
"eslint": "^8.57.1",
"eslint-v8": "npm:eslint@^8.57.1",
"invariant": "^2.2.4",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",

View File

@@ -181,6 +181,7 @@ function ReactForgetFunctionTransform() {
fn,
forgetOptions,
'Other',
'all_features',
'_c',
null,
null,

View File

@@ -11,6 +11,7 @@ import {
injectReanimatedFlag,
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
const ENABLE_REACT_COMPILER_TIMINGS =
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
@@ -61,12 +62,19 @@ export default function BabelPluginReactCompiler(
},
};
}
compileProgram(prog, {
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result?.retryErrors ?? [],
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',

View File

@@ -8,7 +8,119 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {PluginOptions} from './Options';
import {CompilerError} from '../CompilerError';
/**
* Gating rewrite for function declarations which are referenced before their
* declaration site.
*
* ```js
* // original
* export default React.memo(Foo);
* function Foo() { ... }
*
* // React compiler optimized + gated
* import {gating} from 'myGating';
* export default React.memo(Foo);
* const gating_result = gating(); <- inserted
* function Foo_optimized() {} <- inserted
* function Foo_unoptimized() {} <- renamed from Foo
* function Foo() { <- inserted function, which can be hoisted by JS engines
* if (gating_result) return Foo_optimized();
* else return Foo_unoptimized();
* }
* ```
*/
function insertAdditionalFunctionDeclaration(
fnPath: NodePath<t.FunctionDeclaration>,
compiled: t.FunctionDeclaration,
gating: NonNullable<PluginOptions['gating']>,
): void {
const originalFnName = fnPath.node.id;
const originalFnParams = fnPath.node.params;
const compiledParams = fnPath.node.params;
/**
* Note that other than `export default function() {}`, all other function
* declarations must have a binding identifier. Since default exports cannot
* be referenced, it's safe to assume that all function declarations passed
* here will have an identifier.
* https://tc39.es/ecma262/multipage/ecmascript-language-functions-and-classes.html#sec-function-definitions
*/
CompilerError.invariant(originalFnName != null && compiled.id != null, {
reason:
'Expected function declarations that are referenced elsewhere to have a named identifier',
loc: fnPath.node.loc ?? null,
});
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
reason:
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
loc: fnPath.node.loc ?? null,
});
const gatingCondition = fnPath.scope.generateUidIdentifier(
`${gating.importSpecifierName}_result`,
);
const unoptimizedFnName = fnPath.scope.generateUidIdentifier(
`${originalFnName.name}_unoptimized`,
);
const optimizedFnName = fnPath.scope.generateUidIdentifier(
`${originalFnName.name}_optimized`,
);
/**
* Step 1: rename existing functions
*/
compiled.id.name = optimizedFnName.name;
fnPath.get('id').replaceInline(unoptimizedFnName);
/**
* Step 2: insert new function declaration
*/
const newParams: Array<t.Identifier | t.RestElement> = [];
const genNewArgs: Array<() => t.Identifier | t.SpreadElement> = [];
for (let i = 0; i < originalFnParams.length; i++) {
const argName = `arg${i}`;
if (originalFnParams[i].type === 'RestElement') {
newParams.push(t.restElement(t.identifier(argName)));
genNewArgs.push(() => t.spreadElement(t.identifier(argName)));
} else {
newParams.push(t.identifier(argName));
genNewArgs.push(() => t.identifier(argName));
}
}
// insertAfter called in reverse order of how nodes should appear in program
fnPath.insertAfter(
t.functionDeclaration(
originalFnName,
newParams,
t.blockStatement([
t.ifStatement(
gatingCondition,
t.returnStatement(
t.callExpression(
compiled.id,
genNewArgs.map(fn => fn()),
),
),
t.returnStatement(
t.callExpression(
unoptimizedFnName,
genNewArgs.map(fn => fn()),
),
),
),
]),
),
);
fnPath.insertBefore(
t.variableDeclaration('const', [
t.variableDeclarator(
gatingCondition,
t.callExpression(t.identifier(gating.importSpecifierName), []),
),
]),
);
fnPath.insertBefore(compiled);
}
export function insertGatedFunctionDeclaration(
fnPath: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
@@ -18,47 +130,57 @@ export function insertGatedFunctionDeclaration(
| t.ArrowFunctionExpression
| t.FunctionExpression,
gating: NonNullable<PluginOptions['gating']>,
referencedBeforeDeclaration: boolean,
): void {
const gatingExpression = t.conditionalExpression(
t.callExpression(t.identifier(gating.importSpecifierName), []),
buildFunctionExpression(compiled),
buildFunctionExpression(fnPath.node),
);
/*
* Convert function declarations to named variables *unless* this is an
* `export default function ...` since `export default const ...` is
* not supported. For that case we fall through to replacing w the raw
* conditional expression
*/
if (
fnPath.parentPath.node.type !== 'ExportDefaultDeclaration' &&
fnPath.node.type === 'FunctionDeclaration' &&
fnPath.node.id != null
) {
fnPath.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(fnPath.node.id, gatingExpression),
]),
);
} else if (
fnPath.parentPath.node.type === 'ExportDefaultDeclaration' &&
fnPath.node.type !== 'ArrowFunctionExpression' &&
fnPath.node.id != null
) {
fnPath.insertAfter(
t.exportDefaultDeclaration(t.identifier(fnPath.node.id.name)),
);
fnPath.parentPath.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(fnPath.node.id.name),
gatingExpression,
),
]),
);
if (referencedBeforeDeclaration && fnPath.isFunctionDeclaration()) {
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
reason: 'Expected compiled node type to match input type',
description: `Got ${compiled.type} but expected FunctionDeclaration`,
loc: fnPath.node.loc ?? null,
});
insertAdditionalFunctionDeclaration(fnPath, compiled, gating);
} else {
fnPath.replaceWith(gatingExpression);
const gatingExpression = t.conditionalExpression(
t.callExpression(t.identifier(gating.importSpecifierName), []),
buildFunctionExpression(compiled),
buildFunctionExpression(fnPath.node),
);
/*
* Convert function declarations to named variables *unless* this is an
* `export default function ...` since `export default const ...` is
* not supported. For that case we fall through to replacing w the raw
* conditional expression
*/
if (
fnPath.parentPath.node.type !== 'ExportDefaultDeclaration' &&
fnPath.node.type === 'FunctionDeclaration' &&
fnPath.node.id != null
) {
fnPath.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(fnPath.node.id, gatingExpression),
]),
);
} else if (
fnPath.parentPath.node.type === 'ExportDefaultDeclaration' &&
fnPath.node.type !== 'ArrowFunctionExpression' &&
fnPath.node.id != null
) {
fnPath.insertAfter(
t.exportDefaultDeclaration(t.identifier(fnPath.node.id.name)),
);
fnPath.parentPath.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(fnPath.node.id.name),
gatingExpression,
),
]),
);
} else {
fnPath.replaceWith(gatingExpression);
}
}
}

View File

@@ -12,6 +12,7 @@ import {
EnvironmentConfig,
ExternalFunction,
parseEnvironmentConfig,
tryParseExternalFunction,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error';
@@ -271,6 +272,14 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
parsedOptions[key] = parseTargetConfig(value);
break;
}
case 'gating': {
if (value == null) {
parsedOptions[key] = null;
} else {
parsedOptions[key] = tryParseExternalFunction(value);
}
break;
}
default: {
parsedOptions[key] = value;
}

View File

@@ -24,6 +24,7 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -100,6 +101,7 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValiateNoImpureFunctionsInRender';
import {CompilerError} from '..';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -113,6 +115,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
useMemoCacheIdentifier: string,
logger: Logger | null,
filename: string | null,
@@ -122,6 +125,7 @@ function run(
const env = new Environment(
func.scope,
fnType,
mode,
config,
contextIdentifiers,
logger,
@@ -160,10 +164,10 @@ function runWithEnvironment(
validateUseMemo(hir);
if (
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging &&
!env.config.enableMinimalTransformsForRetry
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
@@ -196,8 +200,13 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
}
}
if (env.config.enableFire) {
@@ -205,10 +214,6 @@ function runWithEnvironment(
log({kind: 'hir', name: 'TransformFire', value: hir});
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
}
if (env.config.lowerContextAccess) {
lowerContextAccess(hir, env.config.lowerContextAccess);
}
@@ -219,7 +224,12 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferReferenceEffects(hir);
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
@@ -239,28 +249,30 @@ function runWithEnvironment(
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
}
if (env.config.validateNoSetStateInPassiveEffects) {
validateNoSetStateInPassiveEffects(hir);
}
if (env.config.validateNoSetStateInPassiveEffects) {
validateNoSetStateInPassiveEffects(hir);
}
if (env.config.validateNoJSXInTryStatements) {
validateNoJSXInTryStatement(hir);
}
if (env.config.validateNoJSXInTryStatements) {
validateNoJSXInTryStatement(hir);
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir);
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir);
}
}
inferReactivePlaces(hir);
@@ -280,7 +292,12 @@ function runWithEnvironment(
value: hir,
});
if (!env.config.enableMinimalTransformsForRetry) {
if (env.isInferredMemoEnabled) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
* transform reactive-scope labeled instructions no-ops.
*/
inferReactiveScopeVariables(hir);
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
@@ -529,6 +546,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
useMemoCacheIdentifier: string,
logger: Logger | null,
filename: string | null,
@@ -538,6 +556,7 @@ export function compileFn(
func,
config,
fnType,
mode,
useMemoCacheIdentifier,
logger,
filename,

View File

@@ -16,8 +16,6 @@ import {
EnvironmentConfig,
ExternalFunction,
ReactFunctionType,
MINIMAL_RETRY_CONFIG,
tryParseExternalFunction,
} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
@@ -273,6 +271,9 @@ function isFilePartOfSources(
return false;
}
type CompileProgramResult = {
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
};
/**
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
* exceptions thrown by this function will fail the babel build.
@@ -287,16 +288,16 @@ function isFilePartOfSources(
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass,
): void {
): CompileProgramResult | null {
if (shouldSkipCompilation(program, pass)) {
return;
return null;
}
const environment = pass.opts.environment;
const restrictedImportsErr = validateRestrictedImports(program, environment);
if (restrictedImportsErr) {
handleError(restrictedImportsErr, pass, null);
return;
return null;
}
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
@@ -367,7 +368,7 @@ export function compileProgram(
filename: pass.filename ?? null,
},
);
const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
const processFn = (
fn: BabelFn,
fnType: ReactFunctionType,
@@ -408,6 +409,7 @@ export function compileProgram(
fn,
environment,
fnType,
'all_features',
useMemoCacheIdentifier.name,
pass.opts.logger,
pass.filename,
@@ -418,28 +420,7 @@ export function compileProgram(
compileResult = {kind: 'error', error: err};
}
}
// If non-memoization features are enabled, retry regardless of error kind
if (compileResult.kind === 'error' && environment.enableFire) {
try {
compileResult = {
kind: 'compile',
compiledFn: compileFn(
fn,
{
...environment,
...MINIMAL_RETRY_CONFIG,
},
fnType,
useMemoCacheIdentifier.name,
pass.opts.logger,
pass.filename,
pass.code,
),
};
} catch (err) {
compileResult = {kind: 'error', error: err};
}
}
if (compileResult.kind === 'error') {
/**
* If an opt out directive is present, log only instead of throwing and don't mark as
@@ -450,7 +431,33 @@ export function compileProgram(
} else {
handleError(compileResult.error, pass, fn.node.loc ?? null);
}
return 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',
useMemoCacheIdentifier.name,
pass.opts.logger,
pass.filename,
pass.code,
),
};
} 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;
}
}
pass.opts.logger?.logEvent(pass.filename, {
@@ -539,32 +546,28 @@ export function compileProgram(
program.node.directives,
);
if (moduleScopeOptOutDirectives.length > 0) {
return;
return null;
}
let gating: null | {
gatingFn: ExternalFunction;
referencedBeforeDeclared: Set<CompileResult>;
} = null;
if (pass.opts.gating != null) {
const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
program,
compiledFns.map(result => {
return result.originalFn;
}),
);
if (error) {
handleError(error, pass, null);
return;
}
gating = {
gatingFn: pass.opts.gating,
referencedBeforeDeclared:
getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns),
};
}
const hasLoweredContextAccess = compiledFns.some(
c => c.compiledFn.hasLoweredContextAccess,
);
const externalFunctions: Array<ExternalFunction> = [];
let gating: null | ExternalFunction = null;
try {
// TODO: check for duplicate import specifiers
if (pass.opts.gating != null) {
gating = tryParseExternalFunction(pass.opts.gating);
externalFunctions.push(gating);
if (gating != null) {
externalFunctions.push(gating.gatingFn);
}
const lowerContextAccess = environment.lowerContextAccess;
@@ -601,7 +604,7 @@ export function compileProgram(
}
} catch (err) {
handleError(err, pass, null);
return;
return null;
}
/*
@@ -613,7 +616,12 @@ export function compileProgram(
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
if (gating != null && kind === 'original') {
insertGatedFunctionDeclaration(originalFn, transformedFn, gating);
insertGatedFunctionDeclaration(
originalFn,
transformedFn,
gating.gatingFn,
gating.referencedBeforeDeclared.has(result),
);
} else {
originalFn.replaceWith(transformedFn);
}
@@ -638,6 +646,7 @@ export function compileProgram(
}
addImportsToProgram(program, externalFunctions);
}
return {retryErrors};
}
function shouldSkipCompilation(
@@ -1093,20 +1102,23 @@ function getFunctionName(
}
}
function checkFunctionReferencedBeforeDeclarationAtTopLevel(
function getFunctionReferencedBeforeDeclarationAtTopLevel(
program: NodePath<t.Program>,
fns: Array<BabelFn>,
): CompilerError | null {
const fnIds = new Set(
fns: Array<CompileResult>,
): Set<CompileResult> {
const fnNames = new Map<string, {id: t.Identifier; fn: CompileResult}>(
fns
.map(fn => getFunctionName(fn))
.map<[NodePath<t.Expression> | null, CompileResult]>(fn => [
getFunctionName(fn.originalFn),
fn,
])
.filter(
(name): name is NodePath<t.Identifier> => !!name && name.isIdentifier(),
(entry): entry is [NodePath<t.Identifier>, CompileResult] =>
!!entry[0] && entry[0].isIdentifier(),
)
.map(name => name.node),
.map(entry => [entry[0].node.name, {id: entry[0].node, fn: entry[1]}]),
);
const fnNames = new Map([...fnIds].map(id => [id.name, id]));
const errors = new CompilerError();
const referencedBeforeDeclaration = new Set<CompileResult>();
program.traverse({
TypeAnnotation(path) {
@@ -1132,8 +1144,7 @@ function checkFunctionReferencedBeforeDeclarationAtTopLevel(
* We've reached the declaration, hoisting is no longer possible, stop
* checking for this component name.
*/
if (fnIds.has(id.node)) {
fnIds.delete(id.node);
if (id.node === fn.id) {
fnNames.delete(id.node.name);
return;
}
@@ -1143,21 +1154,13 @@ function checkFunctionReferencedBeforeDeclarationAtTopLevel(
* A null scope means there's no function scope, which means we're at the
* top level scope.
*/
if (scope === null) {
errors.pushErrorDetail(
new CompilerErrorDetail({
reason: `Encountered a function used before its declaration, which breaks Forget's gating codegen due to hoisting`,
description: `Rewrite the reference to ${fn.name} to not rely on hoisting to fix this issue`,
loc: fn.loc ?? null,
suggestions: null,
severity: ErrorSeverity.Invariant,
}),
);
if (scope === null && id.isReferencedIdentifier()) {
referencedBeforeDeclaration.add(fn.fn);
}
},
});
return errors.details.length > 0 ? errors : null;
return referencedBeforeDeclaration;
}
function getReactCompilerRuntimeModule(opts: PluginOptions): string {

View File

@@ -0,0 +1,275 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetailOptions,
EnvironmentConfig,
ErrorSeverity,
Logger,
} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
function throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
{logger, filename}: TraversalState,
): never {
const detail: CompilerErrorDetailOptions = {
...options,
severity: ErrorSeverity.InvalidReact,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail,
});
CompilerError.throw(detail);
}
function assertValidEffectImportReference(
numArgs: number,
paths: Array<NodePath<t.Node>>,
context: TraversalState,
): void {
for (const path of paths) {
const parent = path.parentPath;
if (parent != null && parent.isCallExpression()) {
const args = parent.get('arguments');
/**
* Only error on untransformed references of the form `useMyEffect(...)`
* or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
* TODO: do we also want a mode to also hard error on non-call references?
*/
if (args.length === numArgs) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
path,
context.transformErrors,
);
/**
* Note that we cannot easily check the type of the first argument here,
* as it may have already been transformed by the compiler (and not
* memoized).
*/
throwInvalidReact(
{
reason:
'[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
'This will break your build! ' +
'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: parent.node.loc ?? null,
},
context,
);
}
}
}
}
function assertValidFireImportReference(
paths: Array<NodePath<t.Node>>,
context: TraversalState,
): void {
if (paths.length > 0) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
paths[0],
context.transformErrors,
);
throwInvalidReact(
{
reason:
'[Fire] Untransformed reference to compiler-required feature. ' +
'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: paths[0].node.loc ?? null,
},
context,
);
}
}
export default function validateNoUntransformedReferences(
path: NodePath<t.Program>,
filename: string | null,
logger: Logger | null,
env: EnvironmentConfig,
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
): void {
const moduleLoadChecks = new Map<
string,
Map<string, CheckInvalidReferenceFn>
>();
if (env.enableFire) {
/**
* Error on any untransformed references to `fire` (e.g. including non-call
* expressions)
*/
for (const module of Environment.knownReactModules) {
const react = getOrInsertWith(moduleLoadChecks, module, () => new Map());
react.set('fire', assertValidFireImportReference);
}
}
if (env.inferEffectDependencies) {
for (const {
function: {source, importSpecifierName},
numRequiredArgs,
} of env.inferEffectDependencies) {
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
module.set(
importSpecifierName,
assertValidEffectImportReference.bind(null, numRequiredArgs),
);
}
}
if (moduleLoadChecks.size > 0) {
transformProgram(path, moduleLoadChecks, filename, logger, transformErrors);
}
}
type TraversalState = {
shouldInvalidateScopes: boolean;
program: NodePath<t.Program>;
logger: Logger | null;
filename: string | null;
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>;
};
type CheckInvalidReferenceFn = (
paths: Array<NodePath<t.Node>>,
context: TraversalState,
) => void;
function validateImportSpecifier(
specifier: NodePath<t.ImportSpecifier>,
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
state: TraversalState,
): void {
const imported = specifier.get('imported');
const specifierName: string =
imported.node.type === 'Identifier'
? imported.node.name
: imported.node.value;
const checkFn = importSpecifierChecks.get(specifierName);
if (checkFn == null) {
return;
}
if (state.shouldInvalidateScopes) {
state.shouldInvalidateScopes = false;
state.program.scope.crawl();
}
const local = specifier.get('local');
const binding = local.scope.getBinding(local.node.name);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
loc: local.node.loc ?? null,
});
checkFn(binding.referencePaths, state);
}
function validateNamespacedImport(
specifier: NodePath<t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier>,
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
state: TraversalState,
): void {
if (state.shouldInvalidateScopes) {
state.shouldInvalidateScopes = false;
state.program.scope.crawl();
}
const local = specifier.get('local');
const binding = local.scope.getBinding(local.node.name);
const defaultCheckFn = importSpecifierChecks.get(DEFAULT_EXPORT);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
loc: local.node.loc ?? null,
});
const filteredReferences = new Map<
CheckInvalidReferenceFn,
Array<NodePath<t.Node>>
>();
for (const reference of binding.referencePaths) {
if (defaultCheckFn != null) {
getOrInsertWith(filteredReferences, defaultCheckFn, () => []).push(
reference,
);
}
const parent = reference.parentPath;
if (
parent != null &&
parent.isMemberExpression() &&
parent.get('object') === reference
) {
if (parent.node.computed || parent.node.property.type !== 'Identifier') {
continue;
}
const checkFn = importSpecifierChecks.get(parent.node.property.name);
if (checkFn != null) {
getOrInsertWith(filteredReferences, checkFn, () => []).push(parent);
}
}
}
for (const [checkFn, references] of filteredReferences) {
checkFn(references, state);
}
}
function transformProgram(
path: NodePath<t.Program>,
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
filename: string | null,
logger: Logger | null,
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
): void {
const traversalState: TraversalState = {
shouldInvalidateScopes: true,
program: path,
filename,
logger,
transformErrors,
};
path.traverse({
ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
const importSpecifierChecks = moduleLoadChecks.get(
path.node.source.value,
);
if (importSpecifierChecks == null) {
return;
}
const specifiers = path.get('specifiers');
for (const specifier of specifiers) {
if (specifier.isImportSpecifier()) {
validateImportSpecifier(
specifier,
importSpecifierChecks,
traversalState,
);
} else {
validateNamespacedImport(
specifier as NodePath<
t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier
>,
importSpecifierChecks,
traversalState,
);
}
}
},
});
}
function matchCompilerDiagnostic(
badReference: NodePath<t.Node>,
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
): string | null {
for (const {fn, error} of transformErrors) {
if (fn.isAncestor(badReference)) {
return error.toString();
}
}
return null;
}

View File

@@ -1455,6 +1455,11 @@ function lowerObjectPropertyKey(
kind: 'identifier',
name: key.node.name,
};
} else if (key.isNumericLiteral()) {
return {
kind: 'identifier',
name: String(key.node.value),
};
}
builder.errors.push({

View File

@@ -13,11 +13,13 @@ import {
BlockId,
DependencyPathEntry,
GeneratedSource,
getHookKind,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
InstructionValue,
LoweredFunction,
PropertyLiteral,
ReactiveScopeDependency,
ScopeId,
@@ -112,6 +114,9 @@ export function collectHoistablePropertyLoads(
hoistableFromOptionals,
registry,
nestedFnImmutableContext: null,
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
? new Set()
: getAssumedInvokedFunctions(fn),
});
}
@@ -127,6 +132,11 @@ type CollectHoistablePropertyLoadsContext = {
* but are currently kept separate for readability.
*/
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
/**
* Functions which are assumed to be eventually called (as opposed to ones which might
* not be called, e.g. the 0th argument of Array.map)
*/
assumedInvokedFns: ReadonlySet<LoweredFunction>;
};
function collectHoistablePropertyLoadsImpl(
fn: HIRFunction,
@@ -338,7 +348,13 @@ function collectNonNullsInBlocks(
context.registry.getOrCreateIdentifier(identifier),
);
}
const nodes = new Map<BlockId, BlockInfo>();
const nodes = new Map<
BlockId,
{
block: BasicBlock;
assumedNonNullObjects: Set<PropertyPathNode>;
}
>();
for (const [_, block] of fn.body.blocks) {
const assumedNonNullObjects = new Set<PropertyPathNode>(
knownNonNullIdentifiers,
@@ -358,32 +374,30 @@ function collectNonNullsInBlocks(
) {
assumedNonNullObjects.add(maybeNonNull);
}
if (
(instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod') &&
!fn.env.config.enableTreatFunctionDepsAsConditional
) {
if (instr.value.kind === 'FunctionExpression') {
const innerFn = instr.value.loweredFunc;
const innerHoistableMap = collectHoistablePropertyLoadsImpl(
innerFn.func,
{
...context,
nestedFnImmutableContext:
context.nestedFnImmutableContext ??
new Set(
innerFn.func.context
.filter(place =>
isImmutableAtInstr(place.identifier, instr.id, context),
)
.map(place => place.identifier.id),
),
},
);
const innerHoistables = assertNonNull(
innerHoistableMap.get(innerFn.func.body.entry),
);
for (const entry of innerHoistables.assumedNonNullObjects) {
assumedNonNullObjects.add(entry);
if (context.assumedInvokedFns.has(innerFn)) {
const innerHoistableMap = collectHoistablePropertyLoadsImpl(
innerFn.func,
{
...context,
nestedFnImmutableContext:
context.nestedFnImmutableContext ??
new Set(
innerFn.func.context
.filter(place =>
isImmutableAtInstr(place.identifier, instr.id, context),
)
.map(place => place.identifier.id),
),
},
);
const innerHoistables = assertNonNull(
innerHoistableMap.get(innerFn.func.body.entry),
);
for (const entry of innerHoistables.assumedNonNullObjects) {
assumedNonNullObjects.add(entry);
}
}
}
}
@@ -591,3 +605,130 @@ function reduceMaybeOptionalChains(
}
} while (changed);
}
function getAssumedInvokedFunctions(
fn: HIRFunction,
temporaries: Map<
IdentifierId,
{fn: LoweredFunction; mayInvoke: Set<LoweredFunction>}
> = new Map(),
): ReadonlySet<LoweredFunction> {
const hoistableFunctions = new Set<LoweredFunction>();
/**
* Step 1: Conservatively collect identifier to function expression mappings
*/
for (const block of fn.body.blocks.values()) {
for (const {lvalue, value} of block.instructions) {
/**
* Conservatively only match function expressions which can have guaranteed ssa.
* ObjectMethods and ObjectProperties do not.
*/
if (value.kind === 'FunctionExpression') {
temporaries.set(lvalue.identifier.id, {
fn: value.loweredFunc,
mayInvoke: new Set(),
});
} else if (value.kind === 'StoreLocal') {
const lvalue = value.lvalue.place.identifier;
const maybeLoweredFunc = temporaries.get(value.value.identifier.id);
if (maybeLoweredFunc != null) {
temporaries.set(lvalue.id, maybeLoweredFunc);
}
} else if (value.kind === 'LoadLocal') {
const maybeLoweredFunc = temporaries.get(value.place.identifier.id);
if (maybeLoweredFunc != null) {
temporaries.set(lvalue.identifier.id, maybeLoweredFunc);
}
}
}
}
/**
* Step 2: Forward pass to do analysis of assumed function calls. Note that
* this is conservative and does not count indirect references through
* containers (e.g. `return {cb: () => {...}})`).
*/
for (const block of fn.body.blocks.values()) {
for (const {lvalue, value} of block.instructions) {
if (value.kind === 'CallExpression') {
const callee = value.callee;
const maybeHook = getHookKind(fn.env, callee.identifier);
const maybeLoweredFunc = temporaries.get(callee.identifier.id);
if (maybeLoweredFunc != null) {
// Direct calls
hoistableFunctions.add(maybeLoweredFunc.fn);
} else if (maybeHook != null) {
/**
* Assume arguments to all hooks are safe to invoke
*/
for (const arg of value.args) {
if (arg.kind === 'Identifier') {
const maybeLoweredFunc = temporaries.get(arg.identifier.id);
if (maybeLoweredFunc != null) {
hoistableFunctions.add(maybeLoweredFunc.fn);
}
}
}
}
} else if (value.kind === 'JsxExpression') {
/**
* Assume JSX attributes and children are safe to invoke
*/
for (const attr of value.props) {
if (attr.kind === 'JsxSpreadAttribute') {
continue;
}
const maybeLoweredFunc = temporaries.get(attr.place.identifier.id);
if (maybeLoweredFunc != null) {
hoistableFunctions.add(maybeLoweredFunc.fn);
}
}
for (const child of value.children ?? []) {
const maybeLoweredFunc = temporaries.get(child.identifier.id);
if (maybeLoweredFunc != null) {
hoistableFunctions.add(maybeLoweredFunc.fn);
}
}
} else if (value.kind === 'FunctionExpression') {
/**
* Recursively traverse into other function expressions which may invoke
* or pass already declared functions to react (e.g. as JSXAttributes).
*
* If lambda A calls lambda B, we assume lambda B is safe to invoke if
* lambda A is -- even if lambda B is conditionally called. (see
* `conditional-call-chain` fixture for example).
*/
const loweredFunc = value.loweredFunc.func;
const lambdasCalled = getAssumedInvokedFunctions(
loweredFunc,
temporaries,
);
const maybeLoweredFunc = temporaries.get(lvalue.identifier.id);
if (maybeLoweredFunc != null) {
for (const called of lambdasCalled) {
maybeLoweredFunc.mayInvoke.add(called);
}
}
}
}
if (block.terminal.kind === 'return') {
/**
* Assume directly returned functions are safe to call
*/
const maybeLoweredFunc = temporaries.get(
block.terminal.value.identifier.id,
);
if (maybeLoweredFunc != null) {
hoistableFunctions.add(maybeLoweredFunc.fn);
}
}
}
for (const [_, {fn, mayInvoke}] of temporaries) {
if (hoistableFunctions.has(fn)) {
for (const called of mayInvoke) {
hoistableFunctions.add(called);
}
}
}
return hoistableFunctions;
}

View File

@@ -96,6 +96,8 @@ export const MacroSchema = z.union([
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
@@ -550,8 +552,6 @@ const EnvironmentConfigSchema = z.object({
*/
disableMemoizationForDebugging: z.boolean().default(false),
enableMinimalTransformsForRetry: z.boolean().default(false),
/**
* When true, rather using memoized values, the compiler will always re-compute
* values, and then use a heuristic to compare the memoized value to the newly
@@ -626,17 +626,6 @@ const EnvironmentConfigSchema = z.object({
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
export const MINIMAL_RETRY_CONFIG: PartialEnvironmentConfig = {
validateHooksUsage: false,
validateRefAccessDuringRender: false,
validateNoSetStateInRender: false,
validateNoSetStateInPassiveEffects: false,
validateNoJSXInTryStatements: false,
validateMemoizedEffectDependencies: false,
validateNoCapitalizedCalls: null,
validateBlocklistedImports: null,
enableMinimalTransformsForRetry: true,
};
/**
* For test fixtures and playground only.
*
@@ -663,7 +652,7 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
source: 'react-compiler-runtime',
importSpecifierName: 'shouldInstrument',
},
globalGating: '__DEV__',
globalGating: 'DEV',
},
enableEmitHookGuards: {
source: 'react-compiler-runtime',
@@ -851,6 +840,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
compilerMode: CompilerMode;
useMemoCacheIdentifier: string;
hasLoweredContextAccess: boolean;
hasFireRewrite: boolean;
@@ -861,6 +851,7 @@ export class Environment {
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
logger: Logger | null,
@@ -870,6 +861,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.compilerMode = compilerMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -924,6 +916,10 @@ export class Environment {
this.#hoistedIdentifiers = new Set();
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
}
get nextIdentifierId(): IdentifierId {
return makeIdentifierId(this.#nextIdentifer++);
}
@@ -1125,10 +1121,34 @@ export class Environment {
moduleName.toLowerCase() === 'react-dom'
);
}
static knownReactModules: ReadonlyArray<string> = ['react', 'react-dom'];
getFallthroughPropertyType(
receiver: Type,
_property: Type,
): BuiltInType | PolyType | null {
let shapeId = null;
if (receiver.kind === 'Object' || receiver.kind === 'Function') {
shapeId = receiver.shapeId;
}
if (shapeId !== null) {
const shape = this.#shapes.get(shapeId);
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
loc: null,
suggestions: null,
});
return shape.properties.get('*') ?? null;
}
return null;
}
getPropertyType(
receiver: Type,
property: string,
property: string | number,
): BuiltInType | PolyType | null {
let shapeId = null;
if (receiver.kind === 'Object' || receiver.kind === 'Function') {
@@ -1146,17 +1166,19 @@ export class Environment {
loc: null,
suggestions: null,
});
let value =
shape.properties.get(property) ?? shape.properties.get('*') ?? null;
if (value === null && isHookName(property)) {
value = this.#getCustomHookType();
if (typeof property === 'string') {
return (
shape.properties.get(property) ??
shape.properties.get('*') ??
(isHookName(property) ? this.#getCustomHookType() : null)
);
} else {
return shape.properties.get('*') ?? null;
}
return value;
} else if (isHookName(property)) {
} else if (typeof property === 'string' && isHookName(property)) {
return this.#getCustomHookType();
} else {
return null;
}
return null;
}
getFunctionSignature(type: FunctionType): FunctionSignature | null {

View File

@@ -119,8 +119,8 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
],
/*
* https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.from
* Array.from(arrayLike, optionalFn, optionalThis) not added because
* the Effect of `arrayLike` is polymorphic i.e.
* Array.from(arrayLike, optionalFn, optionalThis)
* Note that the Effect of `arrayLike` is polymorphic i.e.
* - Effect.read if
* - it does not have an @iterator property and is array-like
* (i.e. has a length property)
@@ -128,6 +128,20 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
* - Effect.mutate if it is a self-mutative iterator (e.g. a generator
* function)
*/
[
'from',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [
Effect.ConditionallyMutate,
Effect.ConditionallyMutate,
Effect.ConditionallyMutate,
],
restParam: Effect.Read,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
}),
],
[
'of',
// Array.of(element0, ..., elementN)

View File

@@ -703,6 +703,10 @@ export type ObjectPropertyKey =
| {
kind: 'computed';
name: Place;
}
| {
kind: 'number';
name: number;
};
export type ObjectProperty = {

View File

@@ -535,6 +535,30 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
]);
/**
* MixedReadOnly =
* | primitive
* | simple objects (Record<string, MixedReadOnly>)
* | Array<MixedReadOnly>
*
* APIs such as Relay — but also Flux and other data stores — often return a
* union of types with some interesting properties in terms of analysis.
*
* Given this constraint, if data came from Relay, then we should be able to
* infer things like `data.items.map(): Array`. That may seem like a leap at
* first but remember, we assume you're not patching builtins. Thus the only way
* data.items.map can exist and be a function, given the above set of data types
* and builtin JS methods, is if `data.items` was an Array, and `data.items.map`
* is therefore calling Array.prototype.map. Then we know that function returns
* an Array as well. This relies on the fact that map() is being called, so if
* data.items was some other type it would error at runtime - so it's sound.
*
* Note that this shape is currently only used for hook return values, which
* means that it's safe to type aliasing method-call return kinds as `Frozen`.
*
* Also note that all newly created arrays from method-calls (e.g. `.map`)
* have the appropriate mutable `BuiltInArray` shape
*/
addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
[
'toString',
@@ -546,6 +570,36 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
returnValueKind: ValueKind.Primitive,
}),
],
[
'indexOf',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'includes',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'at',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInMixedReadonlyId},
calleeEffect: Effect.Capture,
returnValueKind: ValueKind.Frozen,
}),
],
[
'map',
addFunction(BUILTIN_SHAPES, [], {
@@ -642,9 +696,9 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
returnType: {kind: 'Object', shapeId: BuiltInMixedReadonlyId},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
returnValueKind: ValueKind.Frozen,
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
}),

View File

@@ -330,6 +330,9 @@ function printObjectPropertyKey(key: ObjectPropertyKey): string {
case 'computed': {
return `[${printPlace(key.name)}]`;
}
case 'number': {
return String(key.name);
}
}
}

View File

@@ -60,7 +60,15 @@ export type PropType = {
kind: 'Property';
objectType: Type;
objectName: string;
propertyName: PropertyLiteral;
propertyName:
| {
kind: 'literal';
value: PropertyLiteral;
}
| {
kind: 'computed';
value: Type;
};
};
export type ObjectMethod = {

View File

@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, ValueKind} from '..';
import {
CompilerError,
CompilerErrorDetailOptions,
ErrorSeverity,
ValueKind,
} from '..';
import {
AbstractValue,
BasicBlock,
@@ -290,21 +295,21 @@ export function inferTerminalFunctionEffects(
return functionEffects;
}
export function raiseFunctionEffectErrors(
export function transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): void {
functionEffects.forEach(eff => {
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
CompilerError.throw(eff.error);
return eff.error;
}
case 'ContextMutation': {
CompilerError.throw({
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
});
};
}
default:
assertExhaustive(

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {Environment} from '../HIR';
import {
AbstractValue,
@@ -49,7 +49,7 @@ import {assertExhaustive} from '../Utils/utils';
import {
inferTerminalFunctionEffects,
inferInstructionFunctionEffects,
raiseFunctionEffectErrors,
transformFunctionEffectErrors,
} from './InferFunctionEffects';
const UndefinedValue: InstructionValue = {
@@ -103,7 +103,7 @@ const UndefinedValue: InstructionValue = {
export default function inferReferenceEffects(
fn: HIRFunction,
options: {isFunctionExpression: boolean} = {isFunctionExpression: false},
): void {
): Array<CompilerErrorDetailOptions> {
/*
* Initial state contains function params
* TODO: include module declarations here as well
@@ -241,8 +241,9 @@ export default function inferReferenceEffects(
if (options.isFunctionExpression) {
fn.effects = functionEffects;
} else if (!fn.env.config.enableMinimalTransformsForRetry) {
raiseFunctionEffectErrors(functionEffects);
return [];
} else {
return transformFunctionEffectErrors(functionEffects);
}
}
@@ -872,11 +873,33 @@ function inferBlock(
reason: new Set([ValueReason.Other]),
context: new Set(),
};
for (const element of instrValue.elements) {
if (element.kind === 'Spread') {
state.referenceAndRecordEffects(
freezeActions,
element.place,
isArrayType(element.place.identifier)
? Effect.Capture
: Effect.ConditionallyMutate,
ValueReason.Other,
);
} else if (element.kind === 'Identifier') {
state.referenceAndRecordEffects(
freezeActions,
element,
Effect.Capture,
ValueReason.Other,
);
} else {
let _: 'Hole' = element.kind;
}
}
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Store;
continuation = {
kind: 'initialize',
valueKind,
effect: {kind: Effect.Capture, reason: ValueReason.Other},
lvalueEffect: Effect.Store,
kind: 'funeffects',
};
break;
}
@@ -1241,21 +1264,12 @@ function inferBlock(
for (let i = 0; i < instrValue.args.length; i++) {
const arg = instrValue.args[i];
const place = arg.kind === 'Identifier' ? arg : arg.place;
if (effects !== null) {
state.referenceAndRecordEffects(
freezeActions,
place,
effects[i],
ValueReason.Other,
);
} else {
state.referenceAndRecordEffects(
freezeActions,
place,
Effect.ConditionallyMutate,
ValueReason.Other,
);
}
state.referenceAndRecordEffects(
freezeActions,
place,
getArgumentEffect(effects != null ? effects[i] : null, arg),
ValueReason.Other,
);
hasCaptureArgument ||= place.effect === Effect.Capture;
}
if (signature !== null) {
@@ -1307,7 +1321,10 @@ function inferBlock(
signature !== null
? {
kind: signature.returnValueKind,
reason: new Set([ValueReason.Other]),
reason: new Set([
signature.returnValueReason ??
ValueReason.KnownReturnSignature,
]),
context: new Set(),
}
: {
@@ -1356,25 +1373,16 @@ function inferBlock(
for (let i = 0; i < instrValue.args.length; i++) {
const arg = instrValue.args[i];
const place = arg.kind === 'Identifier' ? arg : arg.place;
if (effects !== null) {
/*
* If effects are inferred for an argument, we should fail invalid
* mutating effects
*/
state.referenceAndRecordEffects(
freezeActions,
place,
effects[i],
ValueReason.Other,
);
} else {
state.referenceAndRecordEffects(
freezeActions,
place,
Effect.ConditionallyMutate,
ValueReason.Other,
);
}
/*
* If effects are inferred for an argument, we should fail invalid
* mutating effects
*/
state.referenceAndRecordEffects(
freezeActions,
place,
getArgumentEffect(effects != null ? effects[i] : null, arg),
ValueReason.Other,
);
hasCaptureArgument ||= place.effect === Effect.Capture;
}
if (signature !== null) {
@@ -2049,3 +2057,31 @@ function areArgumentsImmutableAndNonMutating(
}
return true;
}
function getArgumentEffect(
signatureEffect: Effect | null,
arg: Place | SpreadPattern,
): Effect {
if (signatureEffect != null) {
if (arg.kind === 'Identifier') {
return signatureEffect;
} else if (
signatureEffect === Effect.Mutate ||
signatureEffect === Effect.ConditionallyMutate
) {
return signatureEffect;
} else {
// see call-spread-argument-mutable-iterator test fixture
if (signatureEffect === Effect.Freeze) {
CompilerError.throwTodo({
reason: 'Support spread syntax for hook arguments',
loc: arg.place.loc,
});
}
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
return Effect.ConditionallyMutate;
}
} else {
return Effect.ConditionallyMutate;
}
}

View File

@@ -2429,6 +2429,9 @@ function codegenObjectPropertyKey(
});
return expr;
}
case 'number': {
return t.numericLiteral(key.name);
}
}
}

View File

@@ -307,11 +307,26 @@ function* generateInstructionTypes(
kind: 'Property',
objectType: value.object.identifier.type,
objectName: getName(names, value.object.identifier.id),
propertyName: value.property,
propertyName: {
kind: 'literal',
value: value.property,
},
});
break;
}
case 'ComputedLoad': {
yield equation(left, {
kind: 'Property',
objectType: value.object.identifier.type,
objectName: getName(names, value.object.identifier.id),
propertyName: {
kind: 'computed',
value: value.property.identifier.type,
},
});
break;
}
case 'MethodCall': {
const returnType = makeType();
yield equation(value.property.identifier.type, {
@@ -336,7 +351,10 @@ function* generateInstructionTypes(
kind: 'Property',
objectType: value.value.identifier.type,
objectName: getName(names, value.value.identifier.id),
propertyName: makePropertyLiteral(propertyName),
propertyName: {
kind: 'literal',
value: makePropertyLiteral(propertyName),
},
});
} else {
break;
@@ -353,7 +371,10 @@ function* generateInstructionTypes(
kind: 'Property',
objectType: value.value.identifier.type,
objectName: getName(names, value.value.identifier.id),
propertyName: makePropertyLiteral(property.key.name),
propertyName: {
kind: 'literal',
value: makePropertyLiteral(property.key.name),
},
});
}
}
@@ -410,7 +431,6 @@ function* generateInstructionTypes(
case 'RegExpLiteral':
case 'MetaProperty':
case 'ComputedStore':
case 'ComputedLoad':
case 'Await':
case 'GetIterator':
case 'IteratorNext':
@@ -454,12 +474,13 @@ class Unifier {
return;
}
const objectType = this.get(tB.objectType);
let propertyType;
if (typeof tB.propertyName === 'number') {
propertyType = null;
} else {
propertyType = this.env.getPropertyType(objectType, tB.propertyName);
}
const propertyType =
tB.propertyName.kind === 'literal'
? this.env.getPropertyType(objectType, tB.propertyName.value)
: this.env.getFallthroughPropertyType(
objectType,
tB.propertyName.value,
);
if (propertyType !== null) {
this.unify(tA, propertyType);
}
@@ -677,7 +698,11 @@ class Unifier {
const RefLikeNameRE = /^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/;
function isRefLikeName(t: PropType): boolean {
return RefLikeNameRE.test(t.objectName) && t.propertyName === 'current';
return (
t.propertyName.kind === 'literal' &&
RefLikeNameRE.test(t.objectName) &&
t.propertyName.value === 'current'
);
}
function tryUnionTypes(ty1: Type, ty2: Type): Type | null {

View File

@@ -0,0 +1,91 @@
## Input
```javascript
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism).
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, (x, idx) => ({...x, id: idx}));
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useIdentity, Stringify } from "shared-runtime";
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism).
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component(t0) {
const $ = _c(4);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr, _temp);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(x, idx) {
return { ...x, id: idx };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
};
```
### Eval output
(kind: ok) <div>{"children":{"value":5,"id":2}}</div>
<div>{"children":{"value":6,"id":2}}</div>
<div>{"children":{"value":6,"id":2}}</div>

View File

@@ -0,0 +1,26 @@
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism).
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, (x, idx) => ({...x, id: idx}));
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};

View File

@@ -0,0 +1,88 @@
## Input
```javascript
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism)
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useIdentity, Stringify } from "shared-runtime";
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism)
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component(t0) {
const $ = _c(4);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
};
```
### Eval output
(kind: ok) <div>{"children":{"value":5}}</div>
<div>{"children":{"value":6}}</div>
<div>{"children":{"value":6}}</div>

View File

@@ -0,0 +1,26 @@
import {useIdentity, Stringify} from 'shared-runtime';
/**
* TODO: Note that this `Array.from` is inferred to be mutating its first
* argument. This is because React Compiler's typing system does not yet support
* annotating a function with a set of argument match cases + distinct
* definitions (polymorphism)
*
* In this case, we should be able to infer that the `Array.from` call is
* not mutating its 0th argument.
* The 0th argument should be typed as having `effect:Mutate` only when
* (1) it might be a mutable iterable or
* (2) the 1st argument might mutate its callee
*/
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};

View File

@@ -0,0 +1,64 @@
## Input
```javascript
import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
function Component(t0) {
const $ = _c(4);
const { value } = t0;
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
let t1;
if ($[0] !== derived) {
t1 = derived.at(-1);
$[0] = derived;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Stringify>{t1}</Stringify>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 5 }],
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
};
```
### Eval output
(kind: ok) <div>{"children":{"value":5,"wat0":"joe"}}</div>
<div>{"children":{"value":6,"wat0":"joe"}}</div>
<div>{"children":{"value":6,"wat0":"joe"}}</div>

View File

@@ -0,0 +1,14 @@
import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity();
const derived = Array.from(arr, mutateAndReturn);
return <Stringify>{derived.at(-1)}</Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 5}],
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
};

View File

@@ -0,0 +1,62 @@
## Input
```javascript
function useBar({arg}) {
/**
* Note that mutableIterator is mutated by the later object spread. Therefore,
* `s.values()` should be memoized within the same block as the object spread.
* In terms of compiler internals, they should have the same reactive scope.
*/
const obj = {};
const s = new Set([obj, 5, 4]);
const mutableIterator = s.values();
const arr = [...mutableIterator];
obj.x = arg;
return arr;
}
export const FIXTURE_ENTRYPOINT = {
fn: useBar,
params: [{arg: 3}],
sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useBar(t0) {
const $ = _c(2);
const { arg } = t0;
let arr;
if ($[0] !== arg) {
const obj = {};
const s = new Set([obj, 5, 4]);
const mutableIterator = s.values();
arr = [...mutableIterator];
obj.x = arg;
$[0] = arg;
$[1] = arr;
} else {
arr = $[1];
}
return arr;
}
export const FIXTURE_ENTRYPOINT = {
fn: useBar,
params: [{ arg: 3 }],
sequentialRenders: [{ arg: 3 }, { arg: 3 }, { arg: 4 }],
};
```
### Eval output
(kind: ok) [{"x":3},5,4]
[{"x":3},5,4]
[{"x":4},5,4]

View File

@@ -0,0 +1,20 @@
function useBar({arg}) {
/**
* Note that mutableIterator is mutated by the later object spread. Therefore,
* `s.values()` should be memoized within the same block as the object spread.
* In terms of compiler internals, they should have the same reactive scope.
*/
const obj = {};
const s = new Set([obj, 5, 4]);
const mutableIterator = s.values();
const arr = [...mutableIterator];
obj.x = arg;
return arr;
}
export const FIXTURE_ENTRYPOINT = {
fn: useBar,
params: [{arg: 3}],
sequentialRenders: [{arg: 3}, {arg: 3}, {arg: 4}],
};

View File

@@ -55,26 +55,20 @@ import { c as _c } from "react/compiler-runtime"; /**
function useBar(t0) {
"use memo";
const $ = _c(3);
const $ = _c(2);
const { arg } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
if ($[0] !== arg) {
const s = new Set([1, 5, 4]);
t1 = s.values();
$[0] = t1;
const mutableIterator = s.values();
t1 = [arg, ...mutableIterator];
$[0] = arg;
$[1] = t1;
} else {
t1 = $[0];
t1 = $[1];
}
const mutableIterator = t1;
let t2;
if ($[1] !== arg) {
t2 = [arg, ...mutableIterator];
$[1] = arg;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {
@@ -84,4 +78,8 @@ export const FIXTURE_ENTRYPOINT = {
};
```
### Eval output
(kind: ok) [3,1,5,4]
[3,1,5,4]
[4,1,5,4]

View File

@@ -1,35 +0,0 @@
## Input
```javascript
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export default ErrorView;
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
const ErrorView = isForgetEnabled_Fixtures()
? (error, _retry) => {
const $ = _c(2);
let t0;
if ($[0] !== error) {
t0 = <MessageBox error={error} />;
$[0] = error;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
: (error, _retry) => <MessageBox error={error}></MessageBox>;
export default ErrorView;
```

View File

@@ -1,4 +0,0 @@
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
export default ErrorView;

View File

@@ -0,0 +1,42 @@
## Input
```javascript
import {useIdentity} from 'shared-runtime';
function useFoo() {
const it = new Set([1, 2]).values();
useIdentity();
return Math.max(...it);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
sequentialRenders: [{}, {}],
};
```
## Code
```javascript
import { useIdentity } from "shared-runtime";
function useFoo() {
const it = new Set([1, 2]).values();
useIdentity();
return Math.max(...it);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
sequentialRenders: [{}, {}],
};
```
### Eval output
(kind: ok) 2
2

View File

@@ -0,0 +1,13 @@
import {useIdentity} from 'shared-runtime';
function useFoo() {
const it = new Set([1, 2]).values();
useIdentity();
return Math.max(...it);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
sequentialRenders: [{}, {}],
};

View File

@@ -21,7 +21,7 @@ import {
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @enableEmitInstrumentForget
function useFoo(props) {
if (__DEV__ && shouldInstrument)
if (DEV && shouldInstrument)
useRenderCounter("useFoo", "/codegen-emit-imports-same-source.ts");
const $ = _c(2);
let t0;

View File

@@ -28,7 +28,7 @@ import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget
function Bar(props) {
"use forget";
if (__DEV__ && shouldInstrument)
if (DEV && shouldInstrument)
useRenderCounter("Bar", "/codegen-instrument-forget-test.ts");
const $ = _c(2);
let t0;
@@ -48,7 +48,7 @@ function NoForget(props) {
function Foo(props) {
"use forget";
if (__DEV__ && shouldInstrument)
if (DEV && shouldInstrument)
useRenderCounter("Foo", "/codegen-instrument-forget-test.ts");
const $ = _c(2);
let t0;

View File

@@ -1,24 +0,0 @@
## Input
```javascript
// @flow @gating
component Foo(ref: React.RefSetter<Controls>) {
return <Bar ref={ref} />;
}
```
## Error
```
1 | // @flow @gating
> 2 | component Foo(ref: React.RefSetter<Controls>) {
| ^^^ Invariant: Encountered a function used before its declaration, which breaks Forget's gating codegen due to hoisting. Rewrite the reference to Foo_withRef to not rely on hoisting to fix this issue (2:2)
3 | return <Bar ref={ref} />;
4 | }
5 |
```

View File

@@ -1,4 +0,0 @@
// @flow @gating
component Foo(ref: React.RefSetter<Controls>) {
return <Bar ref={ref} />;
}

View File

@@ -1,26 +0,0 @@
## Input
```javascript
// @gating
const Foo = React.forwardRef(Foo_withRef);
function Foo_withRef(props, ref) {
return <Bar ref={ref} {...props}></Bar>;
}
```
## Error
```
1 | // @gating
2 | const Foo = React.forwardRef(Foo_withRef);
> 3 | function Foo_withRef(props, ref) {
| ^^^^^^^^^^^ Invariant: Encountered a function used before its declaration, which breaks Forget's gating codegen due to hoisting. Rewrite the reference to Foo_withRef to not rely on hoisting to fix this issue (3:3)
4 | return <Bar ref={ref} {...props}></Bar>;
5 | }
6 |
```

View File

@@ -1,5 +0,0 @@
// @gating
const Foo = React.forwardRef(Foo_withRef);
function Foo_withRef(props, ref) {
return <Bar ref={ref} {...props}></Bar>;
}

View File

@@ -1,24 +0,0 @@
## Input
```javascript
// @gating
import {memo} from 'react';
export default memo(Foo);
function Foo() {}
```
## Error
```
3 |
4 | export default memo(Foo);
> 5 | function Foo() {}
| ^^^ Invariant: Encountered a function used before its declaration, which breaks Forget's gating codegen due to hoisting. Rewrite the reference to Foo to not rely on hoisting to fix this issue (5:5)
6 |
```

View File

@@ -1,5 +0,0 @@
// @gating
import {memo} from 'react';
export default memo(Foo);
function Foo() {}

View File

@@ -0,0 +1,33 @@
## Input
```javascript
import {useIdentity} from 'shared-runtime';
function Component() {
const items = makeArray(0, 1, 2, null, 4, false, 6);
return useIdentity(...items.values());
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [{}, {}],
};
```
## Error
```
3 | function Component() {
4 | const items = makeArray(0, 1, 2, null, 4, false, 6);
> 5 | return useIdentity(...items.values());
| ^^^^^^^^^^^^^^ Todo: Support spread syntax for hook arguments (5:5)
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {
```

View File

@@ -0,0 +1,12 @@
import {useIdentity} from 'shared-runtime';
function Component() {
const items = makeArray(0, 1, 2, null, 4, false, 6);
return useIdentity(...items.values());
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
sequentialRenders: [{}, {}],
};

View File

@@ -4,9 +4,10 @@
```javascript
import {makeArray} from 'shared-runtime';
function Component(props) {
const other = [0, 1];
function Component({}) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
const max = Math.max(...items.filter(Boolean));
const max = Math.max(2, items.push(5), ...other);
return max;
}
@@ -21,13 +22,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
3 | function Component(props) {
4 | const items = makeArray(0, 1, 2, null, 4, false, 6);
> 5 | const max = Math.max(...items.filter(Boolean));
| ^^^^^^^^ Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` (5:5)
6 | return max;
7 | }
8 |
4 | function Component({}) {
5 | const items = makeArray(0, 1, 2, null, 4, false, 6);
> 6 | const max = Math.max(2, items.push(5), ...other);
| ^^^^^^^^ Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` (6:6)
7 | return max;
8 | }
9 |
```

View File

@@ -1,8 +1,9 @@
import {makeArray} from 'shared-runtime';
function Component(props) {
const other = [0, 1];
function Component({}) {
const items = makeArray(0, 1, 2, null, 4, false, 6);
const max = Math.max(...items.filter(Boolean));
const max = Math.max(2, items.push(5), ...other);
return max;
}

View File

@@ -23,11 +23,11 @@ import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.name) {
if ($[0] !== props) {
t0 = function () {
return <div>{props.name}</div>;
};
$[0] = props.name;
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -1,14 +0,0 @@
// @gating @compilationMode(annotation)
export default function Bar(props) {
'use forget';
return <div>{props.bar}</div>;
}
function NoForget(props) {
return <Bar>{props.noForget}</Bar>;
}
export function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
}

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @gating
import {Stringify} from 'shared-runtime';
const ErrorView = ({error, _retry}) => <Stringify error={error}></Stringify>;
export default ErrorView;
export const FIXTURE_ENTRYPOINT = {
fn: eval('ErrorView'),
params: [{}],
};
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { Stringify } from "shared-runtime";
const ErrorView = isForgetEnabled_Fixtures()
? (t0) => {
const $ = _c(2);
const { error } = t0;
let t1;
if ($[0] !== error) {
t1 = <Stringify error={error} />;
$[0] = error;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
: ({ error, _retry }) => <Stringify error={error}></Stringify>;
export default ErrorView;
export const FIXTURE_ENTRYPOINT = {
fn: eval("ErrorView"),
params: [{}],
};
```
### Eval output
(kind: ok) <div>{}</div>

View File

@@ -0,0 +1,10 @@
// @gating
import {Stringify} from 'shared-runtime';
const ErrorView = ({error, _retry}) => <Stringify error={error}></Stringify>;
export default ErrorView;
export const FIXTURE_ENTRYPOINT = {
fn: eval('ErrorView'),
params: [{}],
};

View File

@@ -15,9 +15,22 @@ function NoForget(props) {
function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
if (props.bar < 0) {
return props.children;
}
return (
<Foo bar={props.bar - 1}>
<NoForget />
</Foo>
);
}
global.DEV = true;
export const FIXTURE_ENTRYPOINT = {
fn: eval('Foo'),
params: [{bar: 2}],
};
```
## Code
@@ -29,7 +42,7 @@ import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget
const Bar = isForgetEnabled_Fixtures()
? function Bar(props) {
"use forget";
if (__DEV__ && shouldInstrument)
if (DEV && shouldInstrument)
useRenderCounter("Bar", "/codegen-instrument-forget-gating-test.ts");
const $ = _c(2);
let t0;
@@ -53,23 +66,50 @@ function NoForget(props) {
const Foo = isForgetEnabled_Fixtures()
? function Foo(props) {
"use forget";
if (__DEV__ && shouldInstrument)
if (DEV && shouldInstrument)
useRenderCounter("Foo", "/codegen-instrument-forget-gating-test.ts");
const $ = _c(2);
let t0;
if ($[0] !== props.bar) {
t0 = <Foo>{props.bar}</Foo>;
$[0] = props.bar;
$[1] = t0;
} else {
t0 = $[1];
const $ = _c(3);
if (props.bar < 0) {
return props.children;
}
return t0;
const t0 = props.bar - 1;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <NoForget />;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== t0) {
t2 = <Foo bar={t0}>{t1}</Foo>;
$[1] = t0;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
: function Foo(props) {
"use forget";
return <Foo>{props.bar}</Foo>;
if (props.bar < 0) {
return props.children;
}
return (
<Foo bar={props.bar - 1}>
<NoForget />
</Foo>
);
};
global.DEV = true;
export const FIXTURE_ENTRYPOINT = {
fn: eval("Foo"),
params: [{ bar: 2 }],
};
```
### Eval output
(kind: ok) <div></div>

View File

@@ -11,5 +11,18 @@ function NoForget(props) {
function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
if (props.bar < 0) {
return props.children;
}
return (
<Foo bar={props.bar - 1}>
<NoForget />
</Foo>
);
}
global.DEV = true;
export const FIXTURE_ENTRYPOINT = {
fn: eval('Foo'),
params: [{bar: 2}],
};

View File

@@ -0,0 +1,62 @@
## Input
```javascript
// @flow @gating
import {Stringify} from 'shared-runtime';
import * as React from 'react';
component Foo(ref: React.RefSetter<Controls>) {
return <Stringify ref={ref} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('(...args) => React.createElement(Foo, args)'),
params: [{ref: React.createRef()}],
};
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
import * as React from "react";
const Foo = React.forwardRef(Foo_withRef);
const _isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
function _Foo_withRef_optimized(_$$empty_props_placeholder$$, ref) {
const $ = _c(2);
let t0;
if ($[0] !== ref) {
t0 = <Stringify ref={ref} />;
$[0] = ref;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
function _Foo_withRef_unoptimized(
_$$empty_props_placeholder$$: $ReadOnly<{}>,
ref: React.RefSetter<Controls>,
): React.Node {
return <Stringify ref={ref} />;
}
function Foo_withRef(arg0, arg1) {
if (_isForgetEnabled_Fixtures_result)
return _Foo_withRef_optimized(arg0, arg1);
else return _Foo_withRef_unoptimized(arg0, arg1);
}
export const FIXTURE_ENTRYPOINT = {
fn: eval("(...args) => React.createElement(Foo, args)"),
params: [{ ref: React.createRef() }],
};
```
### Eval output
(kind: ok) <div>{"ref":null}</div>

View File

@@ -0,0 +1,12 @@
// @flow @gating
import {Stringify} from 'shared-runtime';
import * as React from 'react';
component Foo(ref: React.RefSetter<Controls>) {
return <Stringify ref={ref} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('(...args) => React.createElement(Foo, args)'),
params: [{ref: React.createRef()}],
};

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @gating
import {identity, useHook as useRenamed} from 'shared-runtime';
const _ = {
useHook: () => {},
};
identity(_.useHook);
function useHook() {
useRenamed();
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{}],
};
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { identity, useHook as useRenamed } from "shared-runtime";
const _ = {
useHook: isForgetEnabled_Fixtures() ? () => {} : () => {},
};
identity(_.useHook);
const useHook = isForgetEnabled_Fixtures()
? function useHook() {
const $ = _c(1);
useRenamed();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div>hello world!</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
: function useHook() {
useRenamed();
return <div>hello world!</div>;
};
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hello world!</div>

View File

@@ -0,0 +1,16 @@
// @gating
import {identity, useHook as useRenamed} from 'shared-runtime';
const _ = {
useHook: () => {},
};
identity(_.useHook);
function useHook() {
useRenamed();
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{}],
};

View File

@@ -17,6 +17,11 @@ function Foo(props) {
return <Foo>{props.bar}</Foo>;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};
```
## Code
@@ -66,5 +71,12 @@ const Foo = isForgetEnabled_Fixtures()
return <Foo>{props.bar}</Foo>;
};
export const FIXTURE_ENTRYPOINT = {
fn: eval("Bar"),
params: [{ bar: 2 }],
};
```
### Eval output
(kind: ok) <div>2</div>

View File

@@ -12,3 +12,8 @@ function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};

View File

@@ -12,11 +12,23 @@ function NoForget(props) {
return <Bar>{props.noForget}</Bar>;
}
export function Foo(props) {
function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
if (props.bar < 0) {
return props.children;
}
return (
<Foo bar={props.bar - 1}>
<NoForget />
</Foo>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};
```
## Code
@@ -47,25 +59,50 @@ export default Bar;
function NoForget(props) {
return <Bar>{props.noForget}</Bar>;
}
export const Foo = isForgetEnabled_Fixtures()
const Foo = isForgetEnabled_Fixtures()
? function Foo(props) {
"use forget";
const $ = _c(2);
let t0;
if ($[0] !== props.bar) {
t0 = <Foo>{props.bar}</Foo>;
$[0] = props.bar;
$[1] = t0;
} else {
t0 = $[1];
const $ = _c(3);
if (props.bar < 0) {
return props.children;
}
return t0;
const t0 = props.bar - 1;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <NoForget />;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== t0) {
t2 = <Foo bar={t0}>{t1}</Foo>;
$[1] = t0;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
: function Foo(props) {
"use forget";
return <Foo>{props.bar}</Foo>;
if (props.bar < 0) {
return props.children;
}
return (
<Foo bar={props.bar - 1}>
<NoForget />
</Foo>
);
};
export const FIXTURE_ENTRYPOINT = {
fn: eval("Bar"),
params: [{ bar: 2 }],
};
```
### Eval output
(kind: ok) <div>2</div>

View File

@@ -0,0 +1,26 @@
// @gating @compilationMode(annotation)
export default function Bar(props) {
'use forget';
return <div>{props.bar}</div>;
}
function NoForget(props) {
return <Bar>{props.noForget}</Bar>;
}
function Foo(props) {
'use forget';
if (props.bar < 0) {
return props.children;
}
return (
<Foo bar={props.bar - 1}>
<NoForget />
</Foo>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};

View File

@@ -17,6 +17,11 @@ export function Foo(props) {
return <Foo>{props.bar}</Foo>;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};
```
## Code
@@ -66,5 +71,12 @@ export const Foo = isForgetEnabled_Fixtures()
return <Foo>{props.bar}</Foo>;
};
export const FIXTURE_ENTRYPOINT = {
fn: eval("Bar"),
params: [{ bar: 2 }],
};
```
### Eval output
(kind: ok) <div>2</div>

View File

@@ -12,3 +12,8 @@ export function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};

View File

@@ -17,6 +17,11 @@ function Foo(props) {
return <Foo>{props.bar}</Foo>;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};
```
## Code
@@ -65,5 +70,12 @@ const Foo = isForgetEnabled_Fixtures()
return <Foo>{props.bar}</Foo>;
};
export const FIXTURE_ENTRYPOINT = {
fn: eval("Bar"),
params: [{ bar: 2 }],
};
```
### Eval output
(kind: ok) <div>2</div>

View File

@@ -12,3 +12,8 @@ function Foo(props) {
'use forget';
return <Foo>{props.bar}</Foo>;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Bar'),
params: [{bar: 2}],
};

View File

@@ -0,0 +1,61 @@
## Input
```javascript
// @gating
import {createRef, forwardRef} from 'react';
import {Stringify} from 'shared-runtime';
const Foo = forwardRef(Foo_withRef);
function Foo_withRef(props, ref) {
return <Stringify ref={ref} {...props} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('(...args) => React.createElement(Foo, args)'),
params: [{prop1: 1, prop2: 2, ref: createRef()}],
};
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { createRef, forwardRef } from "react";
import { Stringify } from "shared-runtime";
const Foo = forwardRef(Foo_withRef);
const _isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
function _Foo_withRef_optimized(props, ref) {
const $ = _c(3);
let t0;
if ($[0] !== props || $[1] !== ref) {
t0 = <Stringify ref={ref} {...props} />;
$[0] = props;
$[1] = ref;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
}
function _Foo_withRef_unoptimized(props, ref) {
return <Stringify ref={ref} {...props} />;
}
function Foo_withRef(arg0, arg1) {
if (_isForgetEnabled_Fixtures_result)
return _Foo_withRef_optimized(arg0, arg1);
else return _Foo_withRef_unoptimized(arg0, arg1);
}
export const FIXTURE_ENTRYPOINT = {
fn: eval("(...args) => React.createElement(Foo, args)"),
params: [{ prop1: 1, prop2: 2, ref: createRef() }],
};
```
### Eval output
(kind: ok) <div>{"0":{"prop1":1,"prop2":2,"ref":{"current":null}},"ref":"[[ cyclic ref *3 ]]"}</div>

View File

@@ -0,0 +1,13 @@
// @gating
import {createRef, forwardRef} from 'react';
import {Stringify} from 'shared-runtime';
const Foo = forwardRef(Foo_withRef);
function Foo_withRef(props, ref) {
return <Stringify ref={ref} {...props} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('(...args) => React.createElement(Foo, args)'),
params: [{prop1: 1, prop2: 2, ref: createRef()}],
};

View File

@@ -0,0 +1,64 @@
## Input
```javascript
// @gating
import {memo} from 'react';
import {Stringify} from 'shared-runtime';
export default memo(Foo);
function Foo({prop1, prop2}) {
'use memo';
return <Stringify prop1={prop1} prop2={prop2} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Foo'),
params: [{prop1: 1, prop2: 2}],
};
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { memo } from "react";
import { Stringify } from "shared-runtime";
export default memo(Foo);
const _isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
function _Foo_optimized(t0) {
"use memo";
const $ = _c(3);
const { prop1, prop2 } = t0;
let t1;
if ($[0] !== prop1 || $[1] !== prop2) {
t1 = <Stringify prop1={prop1} prop2={prop2} />;
$[0] = prop1;
$[1] = prop2;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function _Foo_unoptimized({ prop1, prop2 }) {
"use memo";
return <Stringify prop1={prop1} prop2={prop2} />;
}
function Foo(arg0) {
if (_isForgetEnabled_Fixtures_result) return _Foo_optimized(arg0);
else return _Foo_unoptimized(arg0);
}
export const FIXTURE_ENTRYPOINT = {
fn: eval("Foo"),
params: [{ prop1: 1, prop2: 2 }],
};
```
### Eval output
(kind: ok) <div>{"prop1":1,"prop2":2}</div>

View File

@@ -0,0 +1,14 @@
// @gating
import {memo} from 'react';
import {Stringify} from 'shared-runtime';
export default memo(Foo);
function Foo({prop1, prop2}) {
'use memo';
return <Stringify prop1={prop1} prop2={prop2} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: eval('Foo'),
params: [{prop1: 1, prop2: 2}],
};

View File

@@ -13,6 +13,11 @@ component Component(value: string) {
export default memo<Props>(Component);
export const FIXTURE_ENTRYPOINT = {
fn: eval('Component'),
params: [{value: 'foo'}],
};
```
## Code
@@ -43,5 +48,12 @@ const Component = isForgetEnabled_Fixtures()
export default memo<Props>(Component);
export const FIXTURE_ENTRYPOINT = {
fn: eval("Component"),
params: [{ value: "foo" }],
};
```
### Eval output
(kind: ok) <div>foo</div>

View File

@@ -8,3 +8,8 @@ component Component(value: string) {
}
export default memo<Props>(Component);
export const FIXTURE_ENTRYPOINT = {
fn: eval('Component'),
params: [{value: 'foo'}],
};

View File

@@ -35,4 +35,7 @@ export default React.forwardRef(
);
```
### Eval output
(kind: exception) Fixture not implemented
logs: ['forwardRef render functions accept exactly two parameters: props and ref. %s','Did you forget to use the ref parameter?']

View File

@@ -0,0 +1,59 @@
## Input
```javascript
// @gating
import * as React from 'react';
let Foo;
const MemoFoo = React.memo(Foo);
Foo = () => <div>hello world!</div>;
/**
* Evaluate this fixture module to assert that compiler + original have the same
* runtime error message.
*/
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [],
};
```
## Code
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import * as React from "react";
let Foo;
const MemoFoo = React.memo(Foo);
Foo = isForgetEnabled_Fixtures()
? () => {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div>hello world!</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
: () => <div>hello world!</div>;
/**
* Evaluate this fixture module to assert that compiler + original have the same
* runtime error message.
*/
export const FIXTURE_ENTRYPOINT = {
fn: isForgetEnabled_Fixtures() ? () => {} : () => {},
params: [],
};
```
### Eval output
(kind: ok)
logs: ['memo: The first argument must be a component. Instead received: %s','undefined']

View File

@@ -0,0 +1,15 @@
// @gating
import * as React from 'react';
let Foo;
const MemoFoo = React.memo(Foo);
Foo = () => <div>hello world!</div>;
/**
* Evaluate this fixture module to assert that compiler + original have the same
* runtime error message.
*/
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [],
};

View File

@@ -3,17 +3,17 @@
```javascript
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
import {Stringify} from 'shared-runtime';
const Renderer = props => (
const ErrorView = (error, _retry) => <Stringify error={error}></Stringify>;
export default props => (
<Foo>
<Bar></Bar>
<ErrorView></ErrorView>
</Foo>
);
export default Renderer;
```
## Code
@@ -21,12 +21,14 @@ export default Renderer;
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { Stringify } from "shared-runtime";
const ErrorView = isForgetEnabled_Fixtures()
? (error, _retry) => {
const $ = _c(2);
let t0;
if ($[0] !== error) {
t0 = <MessageBox error={error} />;
t0 = <Stringify error={error} />;
$[0] = error;
$[1] = t0;
} else {
@@ -34,9 +36,9 @@ const ErrorView = isForgetEnabled_Fixtures()
}
return t0;
}
: (error, _retry) => <MessageBox error={error}></MessageBox>;
: (error, _retry) => <Stringify error={error}></Stringify>;
const Renderer = isForgetEnabled_Fixtures()
export default isForgetEnabled_Fixtures()
? (props) => {
const $ = _c(1);
let t0;
@@ -59,7 +61,8 @@ const Renderer = isForgetEnabled_Fixtures()
<ErrorView></ErrorView>
</Foo>
);
export default Renderer;
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @gating
import {Stringify} from 'shared-runtime';
const ErrorView = (error, _retry) => <Stringify error={error}></Stringify>;
export default props => (
<Foo>
<Bar></Bar>
<ErrorView></ErrorView>
</Foo>
);

View File

@@ -3,15 +3,22 @@
```javascript
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
import {Stringify} from 'shared-runtime';
const ErrorView = (error, _retry) => <Stringify error={error}></Stringify>;
export const Renderer = props => (
<Foo>
<Bar></Bar>
<div>
<span></span>
<ErrorView></ErrorView>
</Foo>
</div>
);
export const FIXTURE_ENTRYPOINT = {
fn: eval('Renderer'),
params: [{}],
};
```
## Code
@@ -19,12 +26,14 @@ export const Renderer = props => (
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { Stringify } from "shared-runtime";
const ErrorView = isForgetEnabled_Fixtures()
? (error, _retry) => {
const $ = _c(2);
let t0;
if ($[0] !== error) {
t0 = <MessageBox error={error} />;
t0 = <Stringify error={error} />;
$[0] = error;
$[1] = t0;
} else {
@@ -32,7 +41,7 @@ const ErrorView = isForgetEnabled_Fixtures()
}
return t0;
}
: (error, _retry) => <MessageBox error={error}></MessageBox>;
: (error, _retry) => <Stringify error={error}></Stringify>;
export const Renderer = isForgetEnabled_Fixtures()
? (props) => {
@@ -40,10 +49,10 @@ export const Renderer = isForgetEnabled_Fixtures()
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<Foo>
<Bar />
<div>
<span />
<ErrorView />
</Foo>
</div>
);
$[0] = t0;
} else {
@@ -52,11 +61,17 @@ export const Renderer = isForgetEnabled_Fixtures()
return t0;
}
: (props) => (
<Foo>
<Bar></Bar>
<div>
<span></span>
<ErrorView></ErrorView>
</Foo>
</div>
);
export const FIXTURE_ENTRYPOINT = {
fn: eval("Renderer"),
params: [{}],
};
```
### Eval output
(kind: ok) <div><span></span><div>{"error":{}}</div></div>

View File

@@ -0,0 +1,16 @@
// @gating
import {Stringify} from 'shared-runtime';
const ErrorView = (error, _retry) => <Stringify error={error}></Stringify>;
export const Renderer = props => (
<div>
<span></span>
<ErrorView></ErrorView>
</div>
);
export const FIXTURE_ENTRYPOINT = {
fn: eval('Renderer'),
params: [{}],
};

View File

@@ -3,15 +3,24 @@
```javascript
// @gating
const ErrorView = (error, _retry) => <MessageBox error={error}></MessageBox>;
import {Stringify} from 'shared-runtime';
export default Renderer = props => (
<Foo>
<Bar></Bar>
const ErrorView = (error, _retry) => <Stringify error={error}></Stringify>;
const Renderer = props => (
<div>
<span></span>
<ErrorView></ErrorView>
</Foo>
</div>
);
export default Renderer;
export const FIXTURE_ENTRYPOINT = {
fn: eval('Renderer'),
params: [{}],
};
```
## Code
@@ -19,12 +28,14 @@ export default Renderer = props => (
```javascript
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
import { c as _c } from "react/compiler-runtime"; // @gating
import { Stringify } from "shared-runtime";
const ErrorView = isForgetEnabled_Fixtures()
? (error, _retry) => {
const $ = _c(2);
let t0;
if ($[0] !== error) {
t0 = <MessageBox error={error} />;
t0 = <Stringify error={error} />;
$[0] = error;
$[1] = t0;
} else {
@@ -32,18 +43,18 @@ const ErrorView = isForgetEnabled_Fixtures()
}
return t0;
}
: (error, _retry) => <MessageBox error={error}></MessageBox>;
: (error, _retry) => <Stringify error={error}></Stringify>;
export default Renderer = isForgetEnabled_Fixtures()
const Renderer = isForgetEnabled_Fixtures()
? (props) => {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<Foo>
<Bar />
<div>
<span />
<ErrorView />
</Foo>
</div>
);
$[0] = t0;
} else {
@@ -52,11 +63,19 @@ export default Renderer = isForgetEnabled_Fixtures()
return t0;
}
: (props) => (
<Foo>
<Bar></Bar>
<div>
<span></span>
<ErrorView></ErrorView>
</Foo>
</div>
);
export default Renderer;
export const FIXTURE_ENTRYPOINT = {
fn: eval("Renderer"),
params: [{}],
};
```
### Eval output
(kind: ok) <div><span></span><div>{"error":{}}</div></div>

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