Compare commits

...

142 Commits

Author SHA1 Message Date
Mofei Zhang
2b817b78bc [compiler] Playground qol: shared compilation option directives with tests
- Adds @compilationMode(all|infer|syntax|annotation) and @panicMode(none) directives. This is now shared with our test infra
- Playground still defaults to `infer` mode while tests default to `all` mode
- See added fixture tests
2025-01-09 12:29:03 -05:00
lauren
8932ca32f4 [playground] Partially revert #32009 (#32035)
I had forgotten that our default error reporting threshold was `none`
due to the fact that build pipelines should not throw errors. This
resets it back to throwing on all errors which mostly is the same as the
eslint plugin.

Closes #32014.
2025-01-09 12:21:05 -05:00
Sebastian Markbåge
c4595ca4c8 Add ViewTransitionComponent to Stacks and DevTools (#32034)
Just adding the name so it shows up.

(Note that no experimental ones are added to the list of filters atm.
Including SuspenseList etc.)
2025-01-09 11:33:34 -05:00
Pieter De Baets
74ea0c73a2 Remove enableGetInspectorDataForInstanceInProduction flag (#32033)
## Summary

Callers for this method has been removed in
65bda54232,
so these methods no longer need to be conditionally exported and the
feature flag can be removed.

## How did you test this change?

Flow fabric/native
2025-01-09 15:51:58 +00:00
Sebastian Markbåge
1506685f0e Suspensey Fonts for View Transition (#32031)
Fonts flickering in while loading can be disturbing to any transition
but especially View Transitions. Even if they don't cause layout thrash
- the paint thrash is bad enough. We might add Suspensey fonts to all
Transitions in the future but it's especially a no-brainer for View
Transitions.

We need to apply mutations to the DOM first to know whether that will
trigger new fonts to load. For general Suspensey fonts, we'd have to
revert the commit by applying mutations in reverse to return to the
previous state. For View Transitions, since a snapshot is already
frozen, we can freeze the screen while we're waiting for the font at no
extra cost. It does mean that the page isn't responsive during this time
but we should only block this for a short period anyway.

The timeout needs to be short enough that it doesn't cause too much of
an issue when it's a new load and slow, yet long enough that you have a
chance to load it. Otherwise we wait for no reason. The assumption here
is that you likely have either cached the font or preloaded it earlier -
or you're on an extremely fast connection. This case is for optimizing
the high end experience.

Before:


https://github.com/user-attachments/assets/e0acfffe-fa49-40d6-82c3-5b08760175fb

After:


https://github.com/user-attachments/assets/615a03d3-9d6b-4eb1-8bd5-182c4c37a628

Note that since the Navigation is blocked on the font now the browser
spinner shows up while the font is loading.
2025-01-09 10:33:44 -05:00
Sebastian Markbåge
fd9cfa416f Execute layout phase before after mutation phase inside view transition (#32029)
This allows mutations and scrolling in the layout phase to be counted
towards the mutation. This would maybe not be the case for gestures but
it is useful for fire-and-forget.

This also avoids the issue that if you resolve navigation in
useLayoutEffect that it ends up dead locked.

It also means that useLayoutEffect does not observe the scroll
restoration and in fact, the scroll restoration would win over any
manual scrolling in layout effects. For better or worse, this is more in
line with how things worked before and how it works in popstate. So it's
less of a breaking change. This does mean that we can't unify the after
mutation phase with the layout phase though.

To do this we need split out flushSpawnedWork from the flushLayoutEffect
call.

Spawned work from setState inside the layout phase is done outside and
not counted towards the transition. They're sync updates and so are not
eligible for their own View Transitions. It's also tricky to support
this since it's unclear what things like exits in that update would
mean. This work will still be able to mutate the live DOM but it's just
not eligible to trigger new transitions or adjust the target of those.

One difference between popstate is that this spawned work is after
scroll restoration. So any scrolling spawned from a second pass would
now win over scroll restoration.

Another consequence of this change is that you can't safely animate
pseudo elements in useLayoutEffect. We'll introduce a better event for
that anyway.
2025-01-08 19:13:25 -05:00
Sebastian Markbåge
800c9db22e ViewTransitions in Navigation (#32028)
This adds navigation support to the View Transition fixture using both
`history.pushState/popstate` and the Navigation API models.

Because `popstate` does scroll restoration synchronously at the end of
the event, but `startViewTransition` cannot start synchronously, it
would observe the "old" state as after applying scroll restoration. This
leads to weird artifacts. So we intentionally do not support View
Transitions in `popstate`. If it suspends anyway for some other reason,
then scroll restoration is broken anyway and then it is supported. We
don't have to do anything here because this is already how things worked
because the sync `popstate` special case already included the sync lane
which opts it out of View Transitions.

For the Navigation API, scroll restoration can be blocked. The best way
to do this is to resolve the Navigation API promise after React has
applied its mutation. We can detect if there's currently any pending
navigation and wait to resolve the `startViewTransition` until it
finishes and any scroll restoration has been applied.


https://github.com/user-attachments/assets/f53b3282-6315-4513-b3d6-b8981d66964e

There is a subtle thing here. If we read the viewport metrics before
scroll restoration has been applied, then we might assume something is
or isn't going to be within the viewport incorrectly. This is evident on
the "Slide In from Left" example. When we're going forward to that page
we shift the scroll position such that it's going to appear in the
viewport. If we did this before applying scroll restoration, it would
not animate because it wasn't in the viewport then. Therefore, we need
to run the after mutation phase after scroll restoration.

A consequence of this is that you have to resolve Navigation in
`useInsertionEffect` as otherwise it leads to a deadlock (which
eventually gets broken by `startViewTransition`'s timeout of 10
seconds). Another consequence is that now `useLayoutEffect` observes the
restored state. However, I think what we'll likely do is move the layout
phase to before the after mutation phase which also ensures that
auto-scrolling inside `useLayoutEffect` are considered in the viewport
measurements as well.
2025-01-08 18:57:54 -05:00
Sebastian Markbåge
98418e8902 [Fiber] Suspend the commit while we wait for the previous View Transition to finish (#32002)
Stacked on #31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.
2025-01-08 13:36:57 -05:00
Sebastian Markbåge
38127b2815 [Fiber] Support only View Transitions v2 (#31996)
Stacked on #31975.

We're going to recommend that the primary way you style a View
Transition is using a View Transition Class (and/or Type). These are
only available in the View Transitions v2 spec. When they're not
available it's better to fallback to just not animating instead of
animating with the wrong styling rules applied.

This is already widely supported in Chrome and Safari 18.2. Safari 18.2
usage is still somewhat low but it's rolling out quickly as we speak.

A way to detect this is by just passing the object form to
`startViewTransition` which throws if it's an earlier version. The
object form is required for `types` but luckily classes rolled out at
the same time. Therefore we're only indirectly detecting class support.

This means that in practice Safari 18.0 and 18.1 won't animate. We could
try to only apply the feature detection if you're actually using classes
or types, but that would create an unfortunate ecosystem burden to try
to support names. It also leads to flaky effects when only some
animations work. Better to just disable them all.

Firefox has yet to ship anything. We'll have to look out for how the
feature detection happens there and if they roll things out in different
order but if you ship late, you deal with web compat as the ball lies.
2025-01-08 13:22:15 -05:00
Sebastian Markbåge
3a5496b3f5 [Fiber] Use className on <ViewTransition> to assign view-transition-class (#31999)
Stacked on #31975.

This is the primary way we recommend styling your View Transitions since
it allows for reusable styling such as a CSS library specializing in
View Transitions in a way that's composable and without naming
conflicts. E.g.

```js
<ViewTransition className="enter-slide-in exit-fade-out update-cross-fade">
```

This doesn't change the HTML `class` attribute. It's not a CSS class.
Instead it assign the `view-transition-class` style prop of the
underlying DOM node while it's transitioning.

You can also just use `<div style={{viewTransitionClass: ...}}>` on the
DOM node but it's convenient to control the Transition completely from
the outside and conceptually we're transitioning the whole fragment. You
can even make Transition components that just wraps existing components.
`<RevealTransition><Component /></RevealTransition>` this way.

Since you can also have multiple wrappers for different circumstances it
allows React's heuristics to use different classes for different
scenarios. We'll likely add more options like configuring different
classes for different `types` or scenarios that can't be described by
CSS alone.

## CSS Modules

```js
import transitions from './transitions.module.css';

<ViewTransition className={transitions.bounceIn}>...</ViewTransition>
```

CSS Modules works well with this strategy because you can have globally
unique namespaces and define your transitions in the CSS modules as a
library that you can import. [As seen in the fixture
here.](8b91b37bb8 (diff-b4d9854171ffdac4d2c01be92a5eff4f8e9e761e6af953094f99ca243b054a85R11))

I did notice an unfortunate bug in how CSS Modules (at least in Webpack)
generates class names. Sometimes the `+` character is used in the hash
of the class name which is not valid for `view-transition-class` and so
it breaks. I had to rename my class names until the hash yielded
something different to work around it. Ideally that bug gets fixed soon.

## className, rly?

`className` isn't exactly the most loved property name, however, I'm
using `className` here too for consistency. Even though in this case
there's no direct equivalent DOM property name. The CSS property is
named `viewTransitionClass`, but the "viewTransition" prefix is implied
by the Component it is on in this case. For most people the fact that
this is actually a different namespace than other CSS classes doesn't
matter. You'll most just use a CSS library anyway and conceptually
you're just assigning classes the same way as `className` on a DOM node.

But if we ever rename the `class` prop then we can do that for this one
as well.
2025-01-08 13:22:06 -05:00
Sebastian Markbåge
a4d122f2d1 Add <ViewTransition> Component (#31975)
This will provide the opt-in for using [View
Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
in React.

View Transitions only trigger for async updates like `startTransition`,
`useDeferredValue`, Actions or `<Suspense>` revealing from fallback to
content. Synchronous updates provide an opt-out but also guarantee that
they commit immediately which View Transitions can't.

There's no need to opt-in to View Transitions at the "cause" side like
event handlers or actions. They don't know what UI will change and
whether that has an animated transition described.

Conceptually the `<ViewTransition>` component is like a DOM fragment
that transitions its children in its own isolate/snapshot. The API works
by wrapping a DOM node or inner component:

```js
import {ViewTransition} from 'react';

<ViewTransition><Component /></ViewTransition>
```

The default is `name="auto"` which will automatically assign a
`view-transition-name` to the inner DOM node. That way you can add a
View Transition to a Component without controlling its DOM nodes styling
otherwise.

A difference between this and the browser's built-in
`view-transition-name: auto` is that switching the DOM nodes within the
`<ViewTransition>` component preserves the same name so this example
cross-fades between the DOM nodes instead of causing an exit and enter:

```js
<ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition>
```

This becomes especially useful with `<Suspense>` as this example
cross-fades between Skeleton and Content:

```js
<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>
```

Where as this example triggers an exit of the Skeleton and an enter of
the Content:

```js
<Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}>
  <ViewTransition><Content /></ViewTransition>
</Suspense>
```

Managing instances and keys becomes extra important.

You can also specify an explicit `name` property for example for
animating the same conceptual item from one page onto another. However,
best practices is to property namespace these since they can easily
collide. It's also useful to add an `id` to it if available.

```js
<ViewTransition name="my-shared-view">
```

The model in general is the same as plain `view-transition-name` except
React manages a set of heuristics for when to apply it. A problem with
the naive View Transitions model is that it overly opts in every
boundary that *might* transition into transitioning. This is leads to
unfortunate effects like things floating around when unrelated updates
happen. This leads the whole document to animate which means that
nothing is clickable in the meantime. It makes it not useful for smaller
and more local transitions. Best practice is to add
`view-transition-name` only right before you're about to need to animate
the thing. This is tricky to manage globally on complex apps and is not
compositional. Instead we let React manage when a `<ViewTransition>`
"activates" and add/remove the `view-transition-name`. This is also when
React calls `startViewTransition` behind the scenes while it mutates the
DOM.

I've come up with a number of heuristics that I think will make a lot
easier to coordinate this. The principle is that only if something that
updates that particular boundary do we activate it. I hope that one day
maybe browsers will have something like these built-in and we can remove
our implementation.

A `<ViewTransition>` only activates if:

- If a mounted Component renders a `<ViewTransition>` within it outside
the first DOM node, and it is within the viewport, then that
ViewTransition activates as an "enter" animation. This avoids inner
"enter" animations trigger when the parent mounts.
- If an unmounted Component had a `<ViewTransition>` within it outside
the first DOM node, and it was within the viewport, then that
ViewTransition activates as an "exit" animation. This avoids inner
"exit" animations triggering when the parent unmounts.
- If an explicitly named `<ViewTransition name="...">` is deep within an
unmounted tree and one with the same name appears in a mounted tree at
the same time, then both are activated as a pair, but only if they're
both in the viewport. This avoids these triggering "enter" or "exit"
animations when going between parents that don't have a pair.
- If an already mounted `<ViewTransition>` is visible and a DOM
mutation, that might affect how it's painted, happens within its
children but outside any nested `<ViewTransition>`. This allows it to
"cross-fade" between its updates.
- If an already mounted `<ViewTransition>` resizes or moves as the
result of direct DOM nodes siblings changing or moving around. This
allows insertion, deletion and reorders into a list to animate all
children. It is only within one DOM node though, to avoid unrelated
changes in the parent to trigger this. If an item is outside the
viewport before and after, then it's skipped to avoid things flying
across the screen.
- If a `<ViewTransition>` boundary changes size, due to a DOM mutation
within it, then the parent activates (or the root document if there are
no more parents). This ensures that the container can cross-fade to
avoid abrupt relayout. This can be avoided by using absolutely
positioned children. When this can avoid bubbling to the root document,
whatever is not animating is still responsive to clicks during the
transition.

Conceptually each DOM node has its own default that activates the parent
`<ViewTransition>` or no transition if the parent is the root. That
means that if you add a DOM node like `<div><ViewTransition><Component
/></ViewTransition></div>` this won't trigger an "enter" animation since
it was the div that was added, not the ViewTransition. Instead, it might
cause a cross-fade of the parent ViewTransition or no transition if it
had no parent. This ensures that only explicit boundaries perform coarse
animations instead of every single node which is really the benefit of
the View Transitions model. This ends up working out well for simple
cases like switching between two pages immediately while transitioning
one floating item that appears on both pages. Because only the floating
item transitions by default.

Note that it's possible to add manual `view-transition-name` with CSS or
`style={{ viewTransitionName: 'auto' }}` that always transitions as long
as something else has a `<ViewTransition>` that activates. For example a
`<ViewTransition>` can wrap a whole page for a cross-fade but inside of
it an explicit name can be added to something to ensure it animates as a
move when something relates else changes its layout. Instead of just
cross-fading it along with the Page which would be the default.

There's more PRs coming with some optimizations, fixes and expanded
APIs. This first PR explores the above core heuristic.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-01-08 12:11:18 -05:00
Sebastian Markbåge
e30c6693e4 [Fiber] Delete isMounted internals (#31966)
The public API has been deleted a long time ago so this should be unused
unless it's used by hacks. It should be replaced with an
effect/lifecycle that manually tracks this if you need it.

The problem with this API is how the timing implemented because it
requires Placement/Hydration flags to be cleared too early. In fact,
that's why we also have a separate PlacementDEV flag that works
differently.


https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberCommitWork.js#L2157-L2165

We should be able to remove this code now.
2025-01-08 12:08:30 -05:00
Ricky
379089d288 [flags] remove enableDeferRootSchedulingToMicrotask (#32008)
Wait for me to merge, but this has landed everywhere and is ready to
remove.
2025-01-08 12:03:01 -05:00
Ruslan Lesiutin
426872679d chore[react-devtools-shell]: disable warnings in dev server overlay (#32005)
Disables warnings Webpack DevServer overlay, which is used by React
DevTools shell.

We are testing against `react-native-web` in this shell, and it installs
older versions of the `react-dom` package, and there are some expected
discrepancies between it and `react-dom` from source.

Before:
![Screenshot 2025-01-07 at 12 50
21](https://github.com/user-attachments/assets/ba7d435e-3265-4446-9994-6a77c6d3d4ef)

After:
![Screenshot 2025-01-07 at 12 49
47](https://github.com/user-attachments/assets/cb45d07c-f561-496a-b76f-bdce3154ab88)
2025-01-08 12:14:52 +00:00
Ricky
a160102f3a [tests] Remove to*Dev matchers (#31989)
Based off: https://github.com/facebook/react/pull/31988

<img width="741" alt="Screenshot 2025-01-06 at 12 52 08 AM"
src="https://github.com/user-attachments/assets/29b159ca-66d4-441f-8817-dd2db66d1edb"
/>

it is done
2025-01-07 14:17:14 -05:00
lauren
f892dabd8c [ci] Make gh workflow names consistent (#32010)
Super minor change to keep our naming scheme consistent for gh workflows
2025-01-07 12:09:10 -05:00
lauren
6efbc0897f [playground] Use default compiler config (#32009)
The playground's compilation mode is currently set to 'all' along with
reporting all errors.

This tends to be misleading since people usually expect a 1:1 match
between how the playground works with what the compiler does in their
codebase, eg https://github.com/reactwg/react-compiler/discussions/51.
2025-01-07 11:53:27 -05:00
Ricky
7b402084af Fix notify target, add lines (#32006) 2025-01-07 09:34:18 -05:00
Ricky
3314162535 bot for pr notifications (#31985)
Going to take some testing to get this right
2025-01-06 17:57:19 -05:00
Ricky
e0c893f51d [assert helpers] ServerIntegration tests (#31988)
Based off: https://github.com/facebook/react/pull/31986
2025-01-06 14:13:03 -05:00
Ricky
6b865330f4 [assert helpers] react-reconciler (#31986)
Based off: https://github.com/facebook/react/pull/31984
2025-01-06 14:12:53 -05:00
Ricky
83be48b9de [tests] fix hidden use() warnings (#31984)
`spyOnDev` is such a footgun.
2025-01-06 14:12:35 -05:00
Sebastian Markbåge
defffdbba4 [Fiber] Don't work on scheduled tasks while we're in an async commit but flush it eagerly if we're sync (#31987)
This is a follow up to #31930 and a prerequisite for #31975.

With View Transitions, the commit phase becomes async which means that
other work can sneak in between. We need to be resilient to that.

This PR first refactors the flushMutationEffects and flushLayoutEffects
to use module scope variables to track its arguments so we can defer
them. It shares these with how we were already doing it for
flushPendingEffects.

We also track how far along the commit phase we are so we know what we
have left to flush.

Then callers of flushPassiveEffects become flushPendingEffects. That
helper synchronously flushes any remaining phases we've yet to commit.
That ensure that things are at least consistent if that happens.

Finally, when we are using a scheduled task, we don't do any work. This
ensures that we're not flushing any work too early if we could've
deferred it. This still ensures that we always do flush it before
starting any new work on any root so new roots observe the committed
state.

There are some unfortunate effects that could happen from allowing
things to flush eagerly. Such as if a flushSync sneaks in before
startViewTransition, it'll skip the animation. If it's during a
suspensey font it'll start the transition before the font has loaded
which might be better than breaking flushSync. It'll also potentially
flush passive effects inside the startViewTransition which should
typically be ok.
2025-01-06 11:30:53 -05:00
lauren
3ce77d55a2 [playground:ci] Don't install compiler deps twice (#31995)
The compiler playground already installs the compiler's dependencies in
a preinstall step. No need to repeat it in CI.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31995).
* __->__ #31995
* #31994
2025-01-06 11:02:23 -05:00
lauren
11df5224e6 [rcr] Generate ts defs (#31994)
This was accidentally removed in the esbuild transition.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31994).
* #31995
* __->__ #31994
2025-01-06 11:01:38 -05:00
Cody Olsen
9627d71c50 fix: react-compiler-runtime should be cjs (#31993) 2025-01-06 09:06:09 -05:00
Miguel Jiménez Esún
301a18a6af react-hooks/rules-of-hooks: detect issues in class properties (#31823)
Co-authored-by: Elizabeth Craig <elcraig@microsoft.com>
2025-01-06 12:12:09 +01:00
Ricky
03e4ec2d0f [assert helpers] react-dom (pt3) (#31983)
moar assert helpers

this finishes all of react-dom except the server integration tests which
are tricky to convert
2025-01-05 17:10:29 -05:00
Ricky
bf883bebbc [fizz] fix empty string href double warning (#31783)
I think this is the suggested change from
https://github.com/facebook/react/pull/31765#discussion_r1884541447

But no tests fail and I'm not sure how to test it? Seems sus. 

Also seems like the `removeAttribute` here should be changed?


9d9f12f269/packages/react-dom-bindings/src/client/ReactDOMComponent.js (L400-L427)
2025-01-03 12:53:28 -05:00
Ricky
f42f8c0635 [flags] Remove enableServerComponentLogs (#31772)
This has landed everywhere.
2025-01-03 12:53:19 -05:00
Sebastian Markbåge
3b009b4cd5 Make RefStatic and LayoutStatic the same bit (#31965)
Refs are basically just fancy Layout Effects. These are conceptually the
same thing and are always visited together so they don't need to be
different flags.

Whenever we disappear/reappear Offscreen content we need to do both Refs
and Layout Effects.

This is just indicating which phase needs to be visited and these are
always the same phase.
2025-01-02 21:23:09 -05:00
lauren
220dece92b [compiler] Switch to esbuild (#31963)
This migrates the compiler's bundler to esbuild instead of rollup.
Unlike React, our bundling use cases are far simpler since the majority
of our packages are meant to be run on node. Rollup was adding
considerable build time overhead whereas esbuild remains fast and has
all the functionality we need out of the box.


### Before
```
time yarn workspaces run build
yarn workspaces v1.22.22

> babel-plugin-react-compiler
yarn run v1.22.22
$ rimraf dist && rollup --config --bundleConfigAsCjs

src/index.ts → dist/index.js...
(!) Circular dependencies
# ...
created dist/index.js in 15.5s
  Done in 16.45s.

> eslint-plugin-react-compiler
yarn run v1.22.22
$ rimraf dist && rollup --config --bundleConfigAsCjs

src/index.ts → dist/index.js...
(!) Circular dependencies
# ...
created dist/index.js in 9.1s
  Done in 10.11s.

> make-read-only-util
yarn run v1.22.22
warning package.json: No license field
$ tsc
  Done in 1.81s.

> react-compiler-healthcheck
yarn run v1.22.22
$ rimraf dist && rollup --config --bundleConfigAsCjs

src/index.ts → dist/index.js...
(!) Circular dependencies
# ...
created dist/index.js in 8.7s
  Done in 10.43s.

> react-compiler-runtime
yarn run v1.22.22
$ rimraf dist && rollup --config --bundleConfigAsCjs

src/index.ts → dist/index.js...
(!) src/index.ts (1:0): Module level directives cause errors when bundled, "use no memo" in "src/index.ts" was ignored.
# ...
created dist/index.js in 1.1s
  Done in 1.82s.

> snap
yarn run v1.22.22
$ rimraf dist && concurrently -n snap,runtime "tsc --build" "yarn --silent workspace react-compiler-runtime build --silent"
$ rimraf dist && rollup --config --bundleConfigAsCjs --silent
[runtime] yarn --silent workspace react-compiler-runtime build --silent exited with code 0
[snap] tsc --build exited with code 0
  Done in 5.73s.
  Done in 47.30s.
yarn workspaces run build  75.92s user 5.48s system 170% cpu 47.821 total
```

### After

```
time yarn workspaces run build
yarn workspaces v1.22.22

> babel-plugin-react-compiler
yarn run v1.22.22
$ rimraf dist && scripts/build.js
  Done in 1.02s.

> eslint-plugin-react-compiler
yarn run v1.22.22
$ rimraf dist && scripts/build.js
  Done in 0.93s.

> make-read-only-util
yarn run v1.22.22
warning package.json: No license field
$ rimraf dist && scripts/build.js
  Done in 0.89s.

> react-compiler-healthcheck
yarn run v1.22.22
$ rimraf dist && scripts/build.js
  Done in 0.58s.

> react-compiler-runtime
yarn run v1.22.22
$ rimraf dist && scripts/build.js
  Done in 0.48s.

> snap
yarn run v1.22.22
$ rimraf dist && concurrently -n snap,runtime "tsc --build" "yarn --silent workspace react-compiler-runtime build"
$ rimraf dist && scripts/build.js
[runtime] yarn --silent workspace react-compiler-runtime build exited with code 0
[snap] tsc --build exited with code 0
  Done in 4.69s.
  Done in 9.46s.
yarn workspaces run build  9.70s user 0.99s system 103% cpu 10.329 total
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31963).
* #31964
* __->__ #31963
* #31962
2025-01-02 16:59:56 -05:00
lauren
c784273bcc [compiler] Update prettier-plugin-hermes-parser (#31962)
Just updating this package.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31962).
* #31964
* #31963
* __->__ #31962
2025-01-02 16:59:45 -05:00
Ricky
dc7578290f [assert helpers] ReactDOMFloat-test (#31901)
Splitting out ReactDOMFloat to it's own PR because it's huge.
2025-01-02 15:53:19 -05:00
Ricky
7c11aad374 [assert helpers] react-dom (pt2) (#31902)
Converts more react-dom tests
2025-01-02 15:53:06 -05:00
Ricky
d8a08f8e39 [assert helpers] ReactDOMComponent-test (#31898)
Splitting out ReactDOMComponent to it's own PR because it's huge.
2025-01-02 15:28:15 -05:00
Ricky
a7c898d83a [assert helpers] react-dom (pt 1) (#31897)
Converts ~half of react-dom tests
2025-01-02 15:28:06 -05:00
Sebastian Markbåge
c81312e3a7 [Fiber] Refactor Commit Phase into Separate Functions for Before Mutation/Mutation/Layout (#31930)
This is doing some general clean up to be able to split the commit root three phases into three separate async steps.
2025-01-02 14:55:34 -05:00
Sebastian Markbåge
d8b903f49e [Fiber] Avoid return value from commitBeforeMutationEffects (#31922)
This is behind an unusual flag (enableCreateEventHandleAPI) that doesn't
serve a special return value. I'll be collecting other flags from this
phase too.

We can just use the global flag and reset it before the next mutation
phase. Unlike focusedInstanceHandle this doesn't leak any memory in the
meantime.
2025-01-02 14:34:26 -05:00
Sebastian Markbåge
6ca7fbe884 [Fiber] Gate Update flag on BeforeMutationMask on flags (#31921)
We're currently visiting the snapshot phase for every `Update` flag even
though we rarely have to do anything in the Snapshot phase.

The only flags that seem to use these wider visits is
`enableCreateEventHandleAPI` and `enableUseEffectEventHook` but really
neither of those should do that neither. They should schedule explicit
Snapshot phases if needed.
2025-01-02 14:34:10 -05:00
Sebastian Markbåge
0de1233fd1 [Fiber] Mark error boundaries and commit phases when an error is thrown (#31876)
This tracks commit phase errors and marks the component that errored as
red. These also get the errors attached to the entry.

<img width="1505" alt="Screenshot 2024-12-20 at 2 40 14 PM"
src="https://github.com/user-attachments/assets/cac3ead7-a024-4e33-ab27-2e95293c4299"
/>

In the render phase I just mark the Error Boundary that caught the
error. We don't have access to the actual error since it's locked behind
closures in the update queue. We could probably expose that someway.

<img width="949" alt="Screenshot 2024-12-20 at 1 49 05 PM"
src="https://github.com/user-attachments/assets/3032455d-d9f2-462b-9c07-7be23663ecd3"
/>

Follow ups:

Since the Error Boundary doesn't commit its attempted render, we don't
log those. If we did then maybe we should just mark the errored
component like I do for the commit phase. We could potentially walk the
list of errors and log the captured fibers and just log their entries as
children.

We could also potentially walk the uncommitted Fiber tree by stashing it
somewhere or even getting it from the alternate. This could be done on
Suspense boundaries too to track failed hydrations.

---------

Co-authored-by: Ricky <rickhanlonii@gmail.com>
2025-01-02 13:28:24 -05:00
Sebastian Markbåge
1e9eb95db5 [Fiber] Mark cascading updates (#31866)
A common source of performance problems is due to cascading renders from
calling `setState` in `useLayoutEffect` or `useEffect`. This marks the
entry from the update to when we start the render as red and `"Cascade"`
to highlight this.

<img width="964" alt="Screenshot 2024-12-19 at 10 54 59 PM"
src="https://github.com/user-attachments/assets/2bfa91e6-1dc1-4b7f-a659-50aaf2a97e83"
/>

In addition to this case, there's another case where you call `setState`
multiple times in the same event causing multiple renders. This might be
due to multiple `flushSync`, or spawned a microtasks from a
`useLayoutEffect`. In theory it could also be from a microtask scheduled
after the first `setState`. This one we can only detect if it's from an
event that has a `window.event` since otherwise it's hard to know if
we're still in the same event.

<img width="1210" alt="Screenshot 2024-12-19 at 11 38 44 PM"
src="https://github.com/user-attachments/assets/ee188bc4-8ebb-4e95-b5a5-4d724856c27d"
/>

I decided against making a ping in a microtask considered a cascade.
Because that should ideally be using the Suspense Optimization and so
wouldn't be considered multi-pass.

<img width="1284" alt="Screenshot 2024-12-19 at 11 07 30 PM"
src="https://github.com/user-attachments/assets/2d173750-a475-41a0-b6cf-679d15c4ca97"
/>

We might consider making the whole render phase and maybe commit phase
red but that should maybe reserved for actual errors. The "Blocked"
phase really represents the `setState` and so will have the stack trace
of the first update.
2025-01-02 13:04:09 -05:00
Sebastian Markbåge
fe21c947c8 [Fiber] Yield every other frame for Transition/Retry work (#31828)
This flag first moves the `shouldYield()` logic into React itself. We
need this for `postTask` compatibility anyway since this logic is no
longer a concern of the scheduler. This means that there can also be no
global `requestPaint()` that asks for painting earlier. So this is best
rolled out with `enableAlwaysYieldScheduler` (and ideally
`enableYieldingBeforePassive`) instead of `enableRequestPaint`.

Once in React we can change the yield timing heuristics. This uses the
previous 5ms for Idle work to keep everything responsive while doing
background work. However, for Transitions and Retries we have seen that
same thread animations (like loading states animating, or constant
animations like cool Three.js stuff) can take CPU time away from the
Transition that causes moving into new content to slow down. Therefore
we only yield every 25ms.

The purpose of this yield is not to avoid the overhead of yielding,
which is very low, but rather to intentionally block any frequently
occurring other main thread work like animations from starving our work.
If we could we could just tell everyone else to throttle their stuff for
ideal scheduling but that's not quite realistic. In other words, the
purpose of this is to reduce the frame rate of animations to 30 fps and
we achieve this by not yielding. We still do yield to allow the
animations to not just stall. This seems like a good balance.

The 5ms of Idle is because we don't really need to yield less often
since the overhead is low. We keep it low to allow 120 fps animations to
run if necessary and our work may not be the only work within a frame so
we need to yield early enough to leave enough time left.

Similarly we choose 25ms rather than say 35ms to ensure that we push
long enough to guarantee to half the frame rate but low enough that
there's plenty of time left for a rAF to power each animation every
other frame. It's also low enough that if something else interrupts the
work like a new interaction, we can still be responsive to that within
50ms or so. We also need to yield in case there's I/O work that needs to
get bounced through the main thread.

This flag is currently off everywhere since we have so many other
scheduling flags but that means there's some urgency to roll those out
fully so we can test this one. There's also some tests to update since
this doesn't go through the Mock scheduler anymore for yields.
2025-01-02 13:02:22 -05:00
lauren
c8c89fab5b [compiler] Update rollup plugins (#31919)
Update our various compiler rollup plugins.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31919).
* #31927
* #31918
* #31917
* #31916
* __->__ #31919
2025-01-02 11:24:26 -05:00
Ruslan Lesiutin
62208bee5a DevTools: fork FastRefresh test for <18 versions of React (#31893)
We currently have a failing test for React DevTools against React 17.
This started failing in https://github.com/facebook/react/pull/30899,
where we changed logic for error tracking and started relying on
`onPostCommitFiberRoot` hook.

Looking at https://github.com/facebook/react/pull/21183,
`onPostCommitFiberRoot` was shipped in 18, which means that any console
errors / warnings emitted in passive effects won't be recorded by React
DevTools for React < 18.
2025-01-02 14:07:21 +00:00
Devon Govett
694d3e1aae [Flight Parcel] Implement prepareDestinationForModule (#31799)
Followup to #31725

This implements `prepareDestinationForModule` in the Parcel Flight
client. On the Parcel side, the `<Resources>` component now only inserts
`<link>` elements for stylesheets (along with a bootstrap script when
needed), and React is responsible for inserting scripts. This ensures
that components that are conditionally dynamic imported during render
are also preloaded.

CSS must be added to the RSC tree using `<Resources>` to avoid FOUC.
This must be manually rendered in both the top-level page, and in any
component that is dynamic imported. It would be nice if there was a way
for React to automatically insert CSS as well, but unfortunately
`prepareDestinationForModule` only knows about client components and not
CSS for server components. Perhaps there could be a way we could
annotate components at code splitting boundaries with the resources they
need? More thoughts in this thread:
https://github.com/facebook/react/pull/31725#discussion_r1884867607
2024-12-31 13:13:43 -05:00
Ruslan Lesiutin
c01b8058e6 DevTools: fix Compiler inegration test with 18.2 (#31904)
Currently failing with `TypeError: Invalid Version: 19`, looks like I've
overlooked this one in https://github.com/facebook/react/pull/31241.
2024-12-29 15:36:21 +00:00
Sebastian Markbåge
50f00fd876 [Flight] Mark Errored Server Components (#31879)
This is similar to #31876 but for Server Components.

It marks them as errored and puts the error message in the Summary
properties.

<img width="1511" alt="Screenshot 2024-12-20 at 5 05 35 PM"
src="https://github.com/user-attachments/assets/92f11e42-0e23-41c7-bfd4-09effb25e024"
/>

This only looks at the current chunk for rejections. That means that
there might still be promises deeper that rejected but it's only the
immediate return value of the Server Component that's considered a
rejection of the component itself.
2024-12-28 02:02:16 -05:00
Sebastian Markbåge
d4ac7689f9 Add Profiler mode to fixtures even if React DevTools is not installed (#31877)
Currently you need to do one of either:

1. Install React DevTools
2. Install React Refresh
3. Add Profiler component

To opt in to component level profiling.

It was a bit confusing that some of the fixtures was doing 2 which made
them work while other was depending on if you had DevTools.

Really React Refresh shouldn't really opt you in I think.
2024-12-28 02:01:49 -05:00
lauren
4309bde2b4 [rcr] Relax react peer dep requirement (#31915)
There's no real reason to restrict the React peer dep to
non-experimental, so relax it.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31915).
* #31919
* #31918
* #31917
* #31916
* __->__ #31915
* #31920
2024-12-27 14:27:43 -05:00
lauren
fc8a898dd1 [compiler] Fix broken fire snapshot (#31920)
This was not committed in #31811
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31920).
* #31919
* #31918
* #31917
* #31916
* #31915
* __->__ #31920
2024-12-26 14:58:37 -05:00
Ricky
97d794958f [assert helpers] Remove toWarnDev from fixtures/dom (#31894)
This is unused and never was:
e6a0473c3c
2024-12-23 18:11:04 -05:00
Ricky
94867f33be [asserts helpers] react package (#31853)
Based off https://github.com/facebook/react/pull/31844

Commit to review:
11aa104e3e

Converts the rest of the `react` package.
2024-12-23 14:58:20 -05:00
Jordan Brown
6907aa2a30 [compiler] Rewrite effect dep arrays that use fire (#31811)
If an effect uses a dep array, also rewrite the dep array to use the
fire binding

--
2024-12-20 17:16:59 -05:00
Jordan Brown
45a720f7c7 [compile] Error on fire outside of effects and ensure correct compilation, correct import (#31798)
Traverse the compiled functions to ensure there are no lingering fires
and that all
fire calls are inside an effect lambda.

Also corrects the import to import from the compiler runtime instead


--
2024-12-20 16:55:01 -05:00
Jordan Brown
ab27231dc5 [compiler] add fire imports (#31797)
Summary:

Adds import {useFire} from 'react' when fire syntax is used.

This is experimentation and may not become a stable feature in the
compiler.

--
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31797).
* #31811
* #31798
* __->__ #31797
2024-12-20 15:25:30 -05:00
Jordan Brown
03297e048d [compiler] transform fire calls (#31796)
This is the diff with the meaningful changes. The approach is:
1. Collect fire callees and remove fire() calls, create a new binding
for the useFire result
2. Update LoadLocals for captured callees to point to the useFire result
3. Update function context to reference useFire results
4. Insert useFire calls after getting to the component scope

This approach aims to minimize the amount of new bindings we introduce
for the function expressions
to minimize bookkeeping for dependency arrays. We keep all of the
LoadLocals leading up to function
calls as they are and insert new instructions to load the originally
captured function, call useFire,
and store the result in a new promoted temporary. The lvalues that
referenced the original callee are
changed to point to the new useFire result.

This is the minimal diff to implement the expected behavior (up to
importing the useFire call, next diff)
and further stacked diffs implement error handling. The rules for fire
are:
1. If you use fire for a callee in the effect once you must use it for
every time you call it in that effect
2. You can only use fire in a useEffect lambda/functions defined inside
the useEffect lambda

There is still more work to do here, like updating the effect dependency
array and handling object methods

--
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31796).
* #31811
* #31798
* #31797
* __->__ #31796
2024-12-20 15:09:09 -05:00
Ricky
99471c02dd [assert helpers] ReactFlight (#31860) 2024-12-20 12:41:30 -05:00
Ricky
26297f5383 [assert helpers] not dom or reconciler (#31862)
converts everything left outside react-dom and react-reconciler
2024-12-20 12:41:13 -05:00
Joseph Savona
6a3d6a4382 [compiler] Allow type cast expressions with refs (#31871)
We report a false positive for the combination of a ref-accessing
function placed inside an array which is they type-cast. Here we teach
ref validation about type casts. I also tried other variants like
`return ref as const` but those already worked.

Closes #31864
2024-12-20 08:56:48 -08:00
Jack Pope
de82912e62 Turn off enableYieldingBeforePassive in internal test renderers (#31863)
https://github.com/facebook/react/pull/31785 turned on
`enableYieldingBeforePassive` for the internal test renderer builds. We
have some failing tests on the RN side blocking the sync so lets turn
these off for now.
2024-12-20 09:48:50 -05:00
Sebastian "Sebbie" Silbermann
518d06d26a Turn off enableYieldingBeforePassive (#31857) 2024-12-19 20:43:01 +01:00
Ricky
36d15d5862 [assert helpers] ReactChildren-test (#31844)
Based off https://github.com/facebook/react/pull/31843

Commit to review:
2c653b81a7

Moar tests
2024-12-19 13:05:23 -05:00
lauren
c70ab3f4b0 [ci] getWorkflowRun should not throw early if workflow hasn't completed (#31861)
We already have handling and retry logic for in-flight workflows in
`downloadArtifactsFromGitHub`, so there's no need to exit early if we
find a workflow for a given commit but it hasn't finished yet.
2024-12-19 13:03:11 -05:00
Sebastian Markbåge
9f540fcc51 [Flight] Support streaming of decodeReply in Edge environments (#31852)
We support streaming `multipart/form-data` in Node.js using Busboy since
that's kind of the idiomatic ecosystem way for handling these stream
there. There's not really anything idiomatic like that for Edge that's
universal yet.

This adds a version that's basically just
`AsyncIterable.from(formData)`. It could also be a `ReadableStream` of
those entries since those are also `AsyncIterable`.

I imagine that in the future we might add one from a binary
`ReadableStream` that does the parsing built-in.
2024-12-19 12:54:59 -05:00
Ricky
8f92ea467e [assert helpers] forwardRef-test (#31843)
Starting to convert the rest of tests to the `assertConsoleTypeDev`
helpers.
2024-12-19 11:50:05 -05:00
Jack Pope
bd76ce54d9 Fork Scheduler feature flags for native-fb (#31859)
#31787 introduces an experimental scheduler flag:
`enableAlwaysYieldScheduler`, which is turned off for www. There wasn't
a SchedulerFeatureFlags fork for native-fb, so the experimental change
was enabled in the Scheduler-dev build there which causes test failures
and is blocking the sync.

#31805 introduces another scheduler flag `enableRequestPaint`, which is
set as a `__VARIANT__` on www. I've set this to `true` here to preserve
the existing behavior. We can follow up with dynamic flags for native-fb
after unblocking the sync.
2024-12-19 11:49:14 -05:00
Andrew Clark
9463d51e51 Update runtime workflow to use HEAD commit (#31850)
This updates the CI workflow for the runtime build and tests to use the
HEAD commit of the PR branch rather than the Fake News merge commit that
the `@actions/checkout` action bafflingly defaults to.

Testing against the merge commit never made sense to me as a behavior
because as soon as someone updates upstream, it's out of date anyway.

It should just match the exact commit that the developer pushed, and the
once that appears in the GitHub UI.
2024-12-19 10:18:06 -05:00
Sebastian Markbåge
a9bbe34622 [Flight] Reject any new Chunks not yet discovered at the time of reportGlobalError (#31851)
Same as #31840 but for the Flight Client.
2024-12-19 00:03:40 -05:00
Sebastian Markbåge
17520b6381 [Fiber] Mark hydrated components in tertiary color (green) (#31829)
This is a follow up to #31752.

This keeps track in the commit phase whether this subtree was hydrated.
If it was, then we mark those components in the Components track as
green. Just like the phase itself is marked as green.

If the boundary client rendered we instead mark it as "errored" and its
children given the plain primary render color (blue). I also collect the
hydration error for this case so we can include its message in the
details view. (Unfortunately this doesn't support newlines atm.)

Most of the time this happens in separate commits for each boundary but
it is possible to force a client render in the same pass as a hydration.
Such as if an update flows into a boundary that has been put into
fallback state after it was initially attempted.

<img width="1487" alt="Screenshot 2024-12-18 at 12 06 54 AM"
src="https://github.com/user-attachments/assets/74c57291-4d11-414c-9751-3dac3285a89a"
/>
2024-12-18 23:53:54 -05:00
lauren
7de040ccfa [ci] Don't cancel runs if more than one branch triggers CI (#31848)
This might primarily only affect those using Sapling for React
development, but pushing the same commit to multiple branches shouldn't
cancel the run

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31848).
* __->__ #31848
* #31847
* #31846
2024-12-18 20:10:03 -05:00
lauren
74e39ce2a1 [ci] Validate downloaded build artifact (#31847)
Adds validation to download-build-artifacts to confirm that the
downloaded artifact matches what was requested.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31847).
* #31848
* __->__ #31847
* #31846
2024-12-18 20:09:50 -05:00
lauren
a34aa05e69 [ci] Allow build artifacts to be downloaded from any branch (#31846)
This was previously scoped to just commits on `main` but this
restriction is unnecessary.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31846).
* #31848
* #31847
* __->__ #31846
2024-12-18 20:09:09 -05:00
Ricky
faf6c4dfdc [flags] Remove debugRenderPhaseSideEffectsForStrictMode (#31839)
This is enabled everywhere, we can just use the inline `__DEV__` checks.
2024-12-18 17:51:12 -05:00
Sebastian Markbåge
ef979d4703 [Flight Reply] Reject any new Chunks not yet discovered at the time of reportGlobalError (#31840)
We might have already resolved models that are not pending and so are
not rejected by aborting the stream. When those later get parsed they
might discover new chunks which end up as pending. These should be
errored since they will never be able to resolve later.

This avoids infinitely hanging the stream.

This same fix needs to be ported to ReactFlightClient that has the same
issue.
2024-12-18 12:56:57 -08:00
Jonathan Hefner
95465dc491 Allow <script> and <template> tags in <select> tag (#31837) 2024-12-18 21:29:16 +01:00
Ricky
74dd2da9ac [flags] Remove enableModernStrictMode (#31838)
This is enabled everywhere.
2024-12-18 14:45:37 -05:00
Ricky
e1d843f4d8 [tests] <StrictMode /> nested in tree is broken (#31825)
Adds a test that shows using <StrictMode /> anywhere outside of the root
node will not fire strict effects.

This works:

```js
root.render(
  <StrictMode>
    <App>
      <Children />
    </App>
  </StrictMode>
);
  ```
  
  This does not fire strict effects on mount:
```js
root.render(
  <App>
    <StrictMode>
      <Children />
    </StrictMode>
  </App>
);
```
2024-12-18 13:29:41 -05:00
Ricky
1e9ef39a87 [flags] Delete enableSchedulerDebugger (#31826)
The tool for this isn't used so I killed it internally and we can clean
up the code to make it easier to reduce the scheduler code.
2024-12-18 13:29:22 -05:00
Hendrik Liebau
7eb8234f7c [Flight] Fix double-incremented pending chunks counter (#31833)
Before calling `emitTimingChunk` inside of `forwardDebugInfo`, we must
not increment `request.pendingChunks`, as this is already done inside of
the `emitTimingChunk` function.

I don't have a unit test for this, but manually verified that this fixes
the hanging responses in https://github.com/vercel/next.js/pull/73804.
2024-12-18 16:35:02 +01:00
David Sancho
2bd1c756c6 Ensure function arity is preserved after build (#31808)
Co-authored-by: eps1lon <sebastian.silbermann@vercel.com>
2024-12-18 14:08:56 +01:00
Sebastian Markbåge
6a4b46cd70 [Fiber] Log Effect and Render Times in Offscreen Commit Phase (#31788)
In https://github.com/facebook/react/pull/30967 and
https://github.com/facebook/react/pull/30983 I added logging of the just
rendered components and the effects. However this didn't consider the
special Offscreen passes. So this adds the same thing to those passes.

Log component effect timings for disconnected/reconnected offscreen
subtrees. This includes initial mount of a Suspense boundary.

Log component render timings for reconnected and already offscreen
offscreen subtrees.
2024-12-17 19:46:03 -05:00
Sebastian Markbåge
facec3ee71 [Fiber] Schedule passive effects using the regular ensureRootIsScheduled flow (#31785)
This treats workInProgressRoot work and rootWithPendingPassiveEffects
the same way. Basically as long as there's some work on the root, yield
the current task. Including passive effects. This means that passive
effects are now a continuation instead of a separate callback. This can
mean they're earlier or later than before. Later for Idle in case
there's other non-React work. Earlier for same Default if there's other
Default priority work.

This makes sense since increasing priority of the passive effects beyond
Idle doesn't really make sense for an Idle render.

However, for any given render at same priority it's more important to
complete this work than start something new.

Since we special case continuations to always yield to the browser, this
has the same effect as #31784 without implementing `requestPaint`. At
least assuming nothing else calls `requestPaint`.

<img width="587" alt="Screenshot 2024-12-14 at 5 37 37 PM"
src="https://github.com/user-attachments/assets/8641b172-8842-4191-8bf0-50cbe263a30c"
/>
2024-12-17 17:01:31 -05:00
Sebastian Markbåge
f5077bcc92 [Scheduler] Always yield to native macro tasks when a virtual task completes (#31787)
As an alternative to #31784.

We should really just always yield each virtual task to a native task.
So that it's 1:1 with native tasks. This affects when microtasks within
each task happens. This brings us closer to native `postTask` semantics
which makes it more seamless to just use that when available.

This still doesn't yield when a task expires to protect against
starvation.
2024-12-17 16:49:01 -05:00
Jack Pope
34ee3919c3 Clean up enableLazyContextPropagation (#31810)
This flag has shipped everywhere, let's clean it up.
2024-12-17 11:56:00 -05:00
Ricky
d428725882 [flags] Clean up scheduler flags (#31814)
These flags are hardcoded now, we can make them static.
2024-12-17 10:27:46 -05:00
Ricky
975cea2d3d Enable debugRenderPhaseSideEffectsForStrictMode in test renderers (#31761)
This flag controls the strict mode double invoke render/lifecycles/etc
behavior in Strict Mode.

The only place this flag is off is the test renderers, which it should
be on for.

If we can land this, we can follow up to remove the flag.
2024-12-16 22:52:18 -05:00
Ricky
49b1a956a9 Enable disableDefaultPropsExceptForClasses (#31804)
TODO: test this PR to see what internal tests fail
2024-12-16 22:51:15 -05:00
Ricky
8dab5920e0 Turn on useModernStrictMode in test renderers (#31769)
It's on everywhere else, let's turn this on so we can remove it. 

Probably should have been turned on in the test renderer for 19.
2024-12-16 22:43:51 -05:00
mofeiZ
8a7b30669a [compiler][ez] Add shape for global Object.keys (#31583)
Add shape / type for global Object.keys. This is useful because
- it has an Effect.Read (not an Effect.Capture) as it cannot alias its
argument.
- Object.keys return an array
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31583).
* __->__ #31583
* #31582
2024-12-16 16:45:17 -05:00
mofeiZ
a78bbf9dbc [compiler] Context variables as dependencies (#31582)
We previously didn't track context variables in the hoistable values
sidemap of `propagateScopeDependencies`. This was overly conservative as
we *do* track the mutable range of context variables, and it is safe to
hoist accesses to context variables after their last direct / aliased
maybe-assignment.

```js
function Component({value}) {
  // start of mutable range for `x`
  let x = DEFAULT;
  const setX = () => x = value;
  const aliasedSet = maybeAlias(setX);
  maybeCall(aliasedSet);
  // end of mutable range for `x`

  // here, we should be able to take x (and property reads
  // off of x) as dependencies
  return <Jsx value={x} />
}
```
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31582).
* #31583
* __->__ #31582
2024-12-16 16:45:05 -05:00
Jordan Brown
c869063f0d [compiler] Add fire to known React APIs (#31795)
Makes `fire` a known export for type-based analysis

--
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31795).
* #31811
* #31798
* #31797
* #31796
* __->__ #31795
* #31794
2024-12-16 15:48:32 -05:00
Jordan Brown
308be6e8dc [compiler] Add option for firing effect functions (#31794)
Config flag for `fire`

--
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31794).
* #31811
* #31798
* #31797
* #31796
* #31795
* __->__ #31794
2024-12-16 15:48:19 -05:00
mofeiZ
d325f872de [compiler][be] Logger based debug printing in test runner (#31809)
Avoid mutable logging enabled state and writing to `process.stdout`
within our babel transform.
2024-12-16 15:15:13 -05:00
mofeiZ
ac17270652 [compiler][ez] Clean up duplicate code in propagateScopeDeps (#31581)
Clean up duplicate checks for when to skip processing values as
dependencies / hoistable temporaries.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31581).
* #31583
* #31582
* __->__ #31581
2024-12-16 15:11:52 -05:00
mofeiZ
80b81fe563 [compiler] Repro for aliased captures within inner function expressions (#31770)
see fixture
2024-12-16 14:43:34 -05:00
mofeiZ
e30872a4e0 [compiler][be] Playground now compiles entire program (#31774)
Compiler playground now runs the entire program through
`babel-plugin-react-compiler` instead of a custom pipeline which
previously duplicated function inference logic from `Program.ts`. In
addition, the playground output reflects the tranformed file (instead of
a "virtual file" of manually concatenated functions).

This helps with the following:
- Reduce potential discrepencies between playground and babel plugin
behavior. See attached fixture output for an example where we previously
diverged.
- Let playground users see compiler-inserted imports (e.g. `_c` or
`useFire`)

This also helps us repurpose playground into a more general tool for
compiler-users instead of just for compiler engineers.
- imports and other functions are preserved.
We differentiate between imports and globals in many cases (e.g.
`inferEffectDeps`), so it may be misleading to omit imports in printed
output
- playground now shows other program-changing behavior like position of
outlined functions and hoisted declarations
- emitted compiled functions do not need synthetic names
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31774).
* #31809
* __->__ #31774
2024-12-16 14:43:21 -05:00
Sebastian Markbåge
54e86bd0d0 [Flight] Color and badge non-primary environments (#31738)
Stacked on #31737.

<img width="987" alt="Screenshot 2024-12-11 at 8 41 15 PM"
src="https://github.com/user-attachments/assets/438379a9-0138-4d02-a53a-419402839558"
/>

When mixing environments (like "use cache" or third party RSC) it's
useful to color and badge those components differently to differentiate.

I'm not putting them in separate tracks because when they do actually
execute, like cache misses or third party RSCs, they behave like they're
part of the same tree.
2024-12-16 13:39:19 -05:00
Sebastian Markbåge
bdf187174d [Flight] Emit Deduped Server Components Marker (#31737)
Stacked on #31736.

<img width="1223" alt="Screenshot 2024-12-11 at 8 21 12 PM"
src="https://github.com/user-attachments/assets/a7cbc04b-c831-476b-aa2f-baddec9461c9"
/>

This emits a placeholder when we're deduping a component. This starts
when the parent's self time ends, where we would've started rendering
this component if it wasn't already started. The end time is when the
actual render ends since the parent is also blocked by it.
2024-12-16 13:16:53 -05:00
Sebastian Markbåge
07facb52d3 [Flight] Sort Server Components Track Group ahead of Client Scheduler/Components Tracks (#31736)
Stacked on #31735.

This ensures that Server Components Track comes first. Since it's
typically rendered first on the server for initial load and then flows
into scheduler and client components work. Also puts it closer to the
Network and further away from "Main" JS.

<img width="769" alt="Screenshot 2024-12-11 at 5 31 41 PM"
src="https://github.com/user-attachments/assets/7198db0f-075e-4a78-8ea4-3bfbf06727cb"
/>

Same trick as in #31615.
2024-12-16 12:39:15 -05:00
Jack Pope
909ed63e0a Clean up context access profiling experiment (#31806)
We introduced the `unstable_useContextWithBailout` API to run compiler
based experiments. This API was designed to be an experiment proxy for
alternative approaches which would be heavier to implement. The
experiment turned out to be inconclusive. Since most of our performance
critical usage is already optimized, we weren't able to find a clear win
with this approach.

Since we don't have further plans for this API, let's clean it up.
2024-12-16 12:32:07 -05:00
Sebastian Markbåge
031230d2e0 [Flight] Stack Parallel Components in Separate Tracks (#31735)
Stacked on https://github.com/facebook/react/pull/31729

<img width="1436" alt="Screenshot 2024-12-11 at 3 36 41 PM"
src="https://github.com/user-attachments/assets/0a201913-0076-4bbf-be18-8f1df6c58313"
/>

The Server Components visualization is currently a tree flame graph
where parent spans the child. This makes it equivalent to the Client
Components visualization.

However, since Server Components can be async and therefore parallel, we
need to do something when two children are executed in parallel. This PR
bumps parallel children into a separate track and then within that track
if that child has more children it can grow within that track.

I currently just cut off more than 10 parallel tracks.

Synchronous Server Components are still in sequence but it's unlikely
because even a simple microtasky Async Component is still parallel.

<img width="959" alt="Screenshot 2024-12-11 at 4 31 17 PM"
src="https://github.com/user-attachments/assets/5ad6a7f8-7fa0-46dc-af51-78caf9849176"
/>

I think this is probably not a very useful visualization for Server
Components but we can try it out.

I'm also going to try a different visualization where parent-child
relationship is horizontal and parallel vertical instead, but it might
not be possible to make that line up in this tool. It makes it a little
harder to see how much different components (including their children)
impact the overall tree. If that's the only visualization it's also
confusing why it's different dimensions than the Client Component
version.
2024-12-16 11:58:25 -05:00
Ricky
f7b1273da2 Flag for requestPaint (#31805)
Will run a quick experiment for this.
2024-12-16 11:18:03 -05:00
Ricky
e06c72fcf4 [flags] Cleanup enableCache (#31778)
This is landed everywhere
2024-12-15 12:34:08 -05:00
Ricky
2d320563f3 [flags] Delete enableDebugTracing (#31780)
This is unused, even in the one builds that uses it, and we don't plan
on landing it in this form.
2024-12-15 12:16:10 -05:00
Sebastian Markbåge
c80b336d23 Implement requestPaint in the actual scheduler (#31784)
When implementing passive effects we did a pretty massive oversight.
While the passive effect is scheduled into its own scheduler task, the
scheduler doesn't always yield to the browser if it has time left. That
means that if you have a fast commit phase, it might try to squeeze in
the passive effects in the same frame but those then might end being
very heavy.

We had `requestPaint()` for this but that was only implemented for the
`isInputPending` experiment. It wasn't thought we needed it for the
regular scheduler because it yields "every frame" anyway - but it
doesn't yield every task. While the `isInputPending` experiment showed
that it wasn't actually any significant impact, and it was better to
keep shorter yield time anyway. Which is why we deleted the code.
Whatever small win it did see in some cases might have been actually due
to this issue rather than anything to do with `isInputPending` at all.

As you can see in https://github.com/facebook/react/pull/31782 we do
have this implemented in the mock scheduler and a lot of behavior that
we assert assumes that this works.

So this just implements yielding after `requestPaint` is called.

Before:

<img width="1023" alt="Screenshot 2024-12-14 at 3 40 24 PM"
src="https://github.com/user-attachments/assets/d60f4bb2-c8f8-4f91-a402-9ac25b278450"
/>

After:

<img width="1108" alt="Screenshot 2024-12-14 at 3 41 25 PM"
src="https://github.com/user-attachments/assets/170cdb90-a049-436f-9501-be3fb9bc04ca"
/>

Notice how in after the native task is split into two. It might not
always actually paint and the native scheduler might make the same
mistake and think it has enough time left but it's at least less likely
to.

We do have another way to do this. When we yield a continuation we also
yield to the native browser. This is to enable the Suspense Optimization
(currently disabled) to work. We could do the same for passive effects
and, in fact, I have a branch that does but because that requires a lot
more tests to be fixed it's a lot more invasive of a change. The nice
thing about this approach is that this is not even running in tests at
all and the tests we do have assert that this is the behavior already. 😬
2024-12-14 16:17:06 -05:00
Sebastian Markbåge
c32780eeb4 [Fiber] Highlight hydration and offscreen render phases (#31752)
This highlights the render phase as the tertiary color (green) when
we're render a hydration lane or offscreen lane.

I call the "Render" phase "Hydrated" instead in this case. For the
offscreen case we don't currently have a differentiation between
hydrated or activity. I just called that "Prepared". Even for the
hydration case where there's no discovered client rendered boundaries
it's more like it's preparing for an interaction rather than blocking
one. Where as for the other lanes the hydration might block something.

<img width="1173" alt="Screenshot 2024-12-12 at 11 23 14 PM"
src="https://github.com/user-attachments/assets/49ab1508-840f-4188-a085-18fe94b14187"
/>

In a follow up I'd like to color the components in the Components tree
green if they were hydrated but not the ones that was actually client
rendered e.g. due to a mismatch or forced client rendering so you can
tell the difference. Unfortunately, the current signals we have for this
get reset earlier in the commit phase than when we log these.

Another thing is that a failed hydration should probably be colored red
even though it ends up committing successfully. I.e. a recoverable
error.
2024-12-14 13:49:47 -05:00
Sebastian Markbåge
d1dd7feabc [Fiber] Schedule client renders using non-hydration lane (#31776)
Related to #31752.

When hydrating, we have two different ways of handling a Suspense
boundary that the server has already given up on and decided to client
render. If we have already hydrated the parent and then later this
happens, then we'll use the retry lane like any ping. If we discover
that it was already in client-render mode when we discover the Suspense
boundary for the first time, then schedule a default lane to let us
first finish the current render and then upgrade the priority to sync to
try to client render this boundary as soon as possible since we're
holding back content.

We used to use the `DefaultHydrationLane` for this but this is not
really a Hydration. It's actually a client render. If we get any other
updates flowing in from above at the same time we might as well do them
in the same pass instead of two passes. So this should be considered
more like any update.

This also means that visually the client render pass now gets painted as
a render instead of a hydration.

This show the flow of a shell being hydrated at the default priority,
then a Suspense boundary being discovered and hydrated at Idle and then
an inner boundary being discovered as client rendered which gets
upgraded to default.

<img width="1363" alt="Screenshot 2024-12-14 at 12 13 57 AM"
src="https://github.com/user-attachments/assets/a141133e-4856-4f38-a11f-f26bd00b6245"
/>
2024-12-14 13:46:21 -05:00
Ricky
9e2c233139 [flags] Delete enableSuspenseAvoidThisFallbackFizz (#31779)
We're not shipping `enableSuspenseAvoidThisFallback` and the fizz flag
is already off so we can delete it.
2024-12-14 13:05:17 -05:00
Brooke
0d67cc0651 Fix commong typo in <title> multiple children error message (#31777) 2024-12-14 12:32:58 -05:00
Ricky
2e25ee373d [flags] Cleanup enableUseMemoCacheHook (#31767)
Based off https://github.com/facebook/react/pull/31766

This has already landed everywhere.
2024-12-14 11:11:04 -05:00
Joseph Savona
a1b3bd0da0 Optimize method calls w props receiver (#31775)
Redo of #31771 without ghstack
2024-12-13 17:10:07 -08:00
Ricky
152080276c Remove enableFlightReadableStream (#31766)
Base: https://github.com/facebook/react/pull/31765

Landed everywhere
2024-12-13 16:39:13 -05:00
Ricky
4996a8fa5c Remove enableFilterEmptyStringAttributesDOM (#31765)
Base off https://github.com/facebook/react/pull/31764

This has landed everywhere
2024-12-13 16:30:10 -05:00
Ricky
3ad17ecd31 Remove enableComponentStackLocations (#31764)
This has landed everywhere
2024-12-13 15:52:42 -05:00
Jack Pope
982cf95c8b Add --cleanup option to flags script to show groups of flags by status (#31762)
`yarn flags --cleanup` will categorize flags to make it more clear which
ones may need to be cleaned up, experiments checked on, or are blocked
by internal rollouts.

Alternative to #31760

<img width="787" alt="Screenshot 2024-12-13 at 2 31 30 PM"
src="https://github.com/user-attachments/assets/452aee7e-9caf-4210-a621-53941d59cb2b"
/>
2024-12-13 15:49:06 -05:00
Ricky
08dfd0b805 Remove enableBinaryflight (#31759)
Based off https://github.com/facebook/react/pull/31757

This has landed everywhere.
2024-12-13 14:50:13 -05:00
Ricky
ef63718a27 Remove enableAsyncActions (#31757)
Based on https://github.com/facebook/react/pull/31756

This is landed everywhere
2024-12-13 13:58:18 -05:00
Ricky
fb12845d77 Remove disableIEWorkarounds (#31756)
Based off https://github.com/facebook/react/pull/31755

This is landed everywhere.
2024-12-13 12:26:40 -05:00
Jack Pope
17ca4b157f Fix useResourceEffect in Fizz (#31758)
We're seeing errors when testing useResourceEffect in SSR and it turns
out we're missing the noop dispatcher function on Fizz.

I tested a local build with this change and it resolved the late
mutation errors in the e2e tests.
2024-12-13 11:26:44 -05:00
Ricky
4dff0e62b2 Remove consoleManagedByDevToolsDuringStrictMode (#31755)
This is enabled everywhere except the test renderers, which don't use
it.
2024-12-13 11:05:10 -05:00
Piotr Tomczewski
a7b829524b [DevTools] Show component names while highlighting renders (#31577)
## Summary
This PR improves the Trace Updates feature by letting developers see
component names directly on the update overlay. Before this change, the
overlay only highlighted updated regions, leaving it unclear which
components were involved. With this update, you can now match visual
updates to their corresponding components, making it much easier to
debug rendering performance.

### New Feature: Show component names while highlighting
When the new **"Show component names while highlighting"** setting is
enabled, the update overlay display the names of affected components
above the rectangles, along with the update count. This gives immediate
context about what’s rendering and why. The preference is stored in
local storage and synced with the backend, so it’s remembered across
sessions.

### Improvements to Drawing Logic
The drawing logic has been updated to make the overlay sharper and
easier to read. Overlay now respect device pixel ratios, so they look
great on high-DPI screens. Outlines have also been made crisper, which
makes it easier to spot exactly where updates are happening.

> [!NOTE]
> **Grouping Logic and Limitations**
> Updates are grouped by their screen position `(left, top coordinates)`
to combine overlapping or nearby regions into a single group. Groups are
sorted by the highest update count within each group, making the most
frequently updated components stand out.
> Overlapping labels may still occur when multiple updates involve
components that overlap but are not in the exact same position. This is
intentional, as the logic aims to maintain a straightforward mapping
between update regions and component names without introducing
unnecessary complexity.

### Testing
This PR also adds tests for the new `groupAndSortNodes` utility, which
handles the logic for grouping and sorting updates. The tests ensure the
behavior is reliable across different scenarios.

## Before & After


https://github.com/user-attachments/assets/6ea0fe3e-9354-44fa-95f3-9a867554f74c


https://github.com/user-attachments/assets/32af4d98-92a5-47dd-a732-f05c2293e41b

---------

Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>
2024-12-13 11:53:05 +00:00
Sebastian Markbåge
56ae4b8d22 Remove unused field _debugIsCurrentlyTiming (#31753)
This field is unused. It's not there at runtime. It's just legacy from
the type.
2024-12-12 23:53:37 -05:00
Sebastian Markbåge
d5e8f79cf4 [Fiber] Use hydration lanes when scheduling hydration work (#31751)
When scheduling the initial root and when using
`unstable_scheduleHydration` we should use the Hydration Lanes rather
than the raw update lane. This ensures that we're always hydrating using
a Hydration Lane or the Offscreen Lane rather than other lanes getting
some random hydration in it.

This fixes an issue where updating a root while it is still hydrating
causes it to trigger client rendering when it could just hydrate and
then apply the update on top of that.

It also fixes a potential performance issue where
`unstable_scheduleHydration` gets batched with an update that then ends
up forcing an update of a boundary that requires it to rewind to do the
hydration lane anyway. Might as well just start with the hydration
without the update applied first.

I added a kill switch (`enableHydrationLaneScheduling`) just in case but
seems very safe given that using `unstable_scheduleHydration` at all is
very rare and updating the root before the shell hydrates is extremely
rare (and used to trigger a recoverable error).
2024-12-12 23:06:07 -05:00
Sebastian Markbåge
7130d0c6cf [Flight Parcel] Remove private package (#31746)
We'll start publishing these.
2024-12-12 15:26:03 -05:00
Sebastian Markbåge
130095f76b [Flight Parcel] Align with more recent changes (#31741)
Follow up to #31725.

I diffed against the Turbopack one to find any unexpected discrepancies.
Some parts are forked enough that it's hard to diff but I think I got
most of it.
2024-12-12 14:39:25 -05:00
Jack Pope
e854ce3b15 Fix canary version strings (#31721)
We're still publishing RCs and creating canary version strings using the
RC naming convention. Setting the `canaryChannelLabel` back to canary
fixes the version names and tags after the 19 stable release.
2024-12-12 14:11:24 -05:00
Andrew Clark
c86542b240 Bump next prerelease version numbers (#31676)
Updates the version numbers in the prerelease (canary and experimental)
channels.

---------

Co-authored-by: Jack Pope <jackpope1@gmail.com>
2024-12-12 14:10:46 -05:00
Sebastian Markbåge
6928bf2f7c [Flight] Log Server Component into Performance Track (#31729)
<img width="966" alt="Screenshot 2024-12-10 at 10 49 19 PM"
src="https://github.com/user-attachments/assets/27a21bdf-86b9-4203-893b-89523e698138">

This emits a tree view visualization of the timing information for each
Server Component provided in the RSC payload.

The unique thing about this visualization is that the end time of each
Server Component spans the end of the last child. Now what is
conceptually a blocking child is kind of undefined in RSC. E.g. if
you're not using a Promise on the client, or if it is wrapped in
Suspense, is it really blocking the parent?

Here I reconstruct parent-child relationship by which chunks reference
other chunks. A child can belong to more than one parent like when we
dedupe the result of a Server Component.

Then I wait until the whole RSC payload has streamed in, and then I
traverse the tree collecting the end time from children as I go and emit
the `performance.measure()` calls on the way up.

There's more work for this visualization in follow ups but this is the
basics. For example, since the Server Component time span includes async
work it's possible for siblings to execute their span in parallel (Foo
and Bar in the screenshot are parallel siblings). To deal with this we
need to spawn parallel work into separate tracks. Each one can be deep
due to large trees. This can makes this type of visualization unwieldy
when you have a lot of parallelism. Therefore I also plan another
flatter Timeline visualization in a follow up.
2024-12-12 14:03:18 -05:00
Devon Govett
ca587425fe Implement react-server-dom-parcel (#31725)
This adds a new `react-server-dom-parcel-package`, which is an RSC
integration for the Parcel bundler. It is mostly copied from the
existing webpack/turbopack integrations, with some changes to utilize
Parcel runtime APIs for loading and executing bundles/modules.

See https://github.com/parcel-bundler/parcel/pull/10043 for the Parcel
side of this, which includes the plugin needed to generate client and
server references. https://github.com/parcel-bundler/rsc-examples also
includes examples of various ways to use RSCs with Parcel.

Differences from other integrations:

* Client and server modules are all part of the same graph, and we use
Parcel's
[environments](https://parceljs.org/plugin-system/transformer/#the-environment)
to distinguish them. The server is the Parcel build entry point, and it
imports and renders server components in route handlers. When a `"use
client"` directive is seen, the environment changes and Parcel creates a
new client bundle for the page, combining all client modules together.
CSS from both client and server components are also combined
automatically.
* There is no separate manifest file that needs to be passed around by
the user. A [Runtime](https://parceljs.org/plugin-system/runtime/)
plugin injects client and server references as needed into the relevant
bundles, and registers server action ids using `react-server-dom-parcel`
automatically.
* A special `<Resources>` component is also generated by Parcel to
render the `<script>` and `<link rel="stylesheet">` elements needed for
a page, using the relevant info from the bundle graph.

Note: I've already published a 0.0.x version of this package to npm for
testing purposes but happy to add whoever needs access to it as well.

### Questions

* How to test this in the React repo. I'll have integration tests in
Parcel, but setting up all the different mocks and environments to
simulate that here seems challenging. I could try to copy how
Webpack/Turbopack do it but it's a bit different.
* Where to put TypeScript types. Right now I have some ambient types in
my [example
repo](https://github.com/parcel-bundler/rsc-examples/blob/main/types.d.ts)
but it would be nice for users not to copy and paste these. Can I
include them in the package or do they need to maintained separately in
definitelytyped? I would really prefer not to have to maintain code in
three different repos ideally.

---------

Co-authored-by: Sebastian Markbage <sebastian@calyptus.eu>
2024-12-11 22:58:51 -05:00
Noah Lemen
a4964987dc Make enableOwnerStacks dynamic (#31661)
following up on https://github.com/facebook/react/pull/31287, fixing
tests

---------

Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
2024-12-11 12:00:25 -05:00
Alex Hunt
92b62f500c Remove comment syntax from ReactNativeTypes (#31457)
# Summary

I'm working to get the main `react-native` package parsable by modern
Flow tooling (both `flow-bundler`, `flow-api-translator`).

This diff trivially removes some redundant Flow comment syntax in
`ReactNativeTypes.js`, which fixes parsing under these newer tools.

## How did you test this change?

Files were pasted into `react-native-github` under fbsource, where Flow
validates .
2024-12-11 16:55:11 +00:00
Sebastian Markbåge
79ddf5b574 [Flight] Track Timing Information (#31716)
Stacked on #31715.

This adds profiling data for Server Components to the RSC stream (but
doesn't yet use it for anything). This is on behind
`enableProfilerTimer` which is on for Dev and Profiling builds. However,
for now there's no Profiling build of Flight so in practice only in DEV.
It's gated on `enableComponentPerformanceTrack` which is experimental
only for now.

We first emit a timeOrigin in the beginning of the stream. This provides
us a relative time to emit timestamps against for cross environment
transfer so that we can log it in terms of absolute times. Using this as
a separate field allows the actual relative timestamps to be a bit more
compact representation and preserves floating point precision.

We emit a timestamp before emitting a Server Component which represents
the start time of the Server Component. The end time is either when the
next Server Component starts or when we finish the task.

We omit the end time for simple tasks that are outlined without Server
Components.

By encoding this as part of the debugInfo stream, this information can
be forwarded between Server to Server RSC.
2024-12-10 20:46:19 -05:00
Marin Atanasov
7c4a7c9ddf react-hooks/rules-of-hooks: Improve support for do/while loops (#31720) 2024-12-10 22:46:33 +01:00
Jack Pope
16367ceb02 [compiler] Fix dropped ref with spread props in InlineJsxTransform (#31726)
When supporting ref as prop in
https://github.com/facebook/react/pull/31558, I missed fixing the
optimization to pass a spread-props-only props object in without an
additional object copy. In the case that we have only a ref along with a
spread, we cannot return only the spread object. This results in
dropping the ref.

In this example
```javascript
<Foo ref={ref} {...props} />
```

The bugged output is:
```javascript
{
  // ...
  props: props
}
```

With this change we now get the correct output:
```javascript
{
  // ...
  props: {ref: ref, ...props}
}
```
2024-12-10 16:11:17 -05:00
Josh Story
7cb356e862 [Flight] rename prerender to unstable_prerender and include in stable channel (#31724)
We added an experimental `prerender` API to flight. This change exposes
this API in stable channels prefixed as `unstable_prerender`. We have
high confidence this API should exist but because we have not yet
settled on how to handle resuming/replaying of RSC streams we may need
to change the API contract to suit future needs. This release will allow
us to get more usage out of the existing implemented functionality
without requiring you to use experimental builds which will open up
greater adoption and opportunity for feedback.

the `prerender` implementation is documented in the `react-server`
package. As with all RSC APIs implemented in bundler specific binding
packages these aren't intended to be called by end users but instead be
used by frameworks implementing React Server Components.

Previously `prerender` was exposed unprefixed and only in the
experimental channel. This PR renames the export across all channels to
`unstable_prerender` so users of this previously unprefixed api will
need to update to the unstable form. This isn't a breaking change
because it was only exposed in the experimental channel which does not
follow semver. The reason we don't expose it under both names is that
users may feature detect the unprefixed form and then when we finally do
ship it as unprefixed we may change the function signature and break
this code. Changing the name now is much safer.
2024-12-10 11:51:39 -08:00
Sebastian Markbåge
4a8fc0f92e [Flight] Don't call onError/onPostpone when halting and unify error branches (#31715)
We shouldn't call onError/onPostpone when we halt a stream because that
node didn't error yet. Its digest would also get lost.

We also have a lot of error branches now for thenables and streams. This
unifies them under erroredTask. I'm not yet unifying the cases that
don't allocate a task for the error when those are outlined.
2024-12-10 11:59:50 -05:00
Sebastian Markbåge
3b597c0576 Clean up findFiberByHostInstance from DevTools Hook (#31711)
The need for this was removed in
https://github.com/facebook/react/pull/30831

Since the new DevTools version has been released for a while and we
expect people to more or less auto-update. Future versions of React
don't need this.

Once we remove the remaining uses of `getInstanceFromNode` e.g. in the
deprecated internal `findDOMNode`/`findNodeHandle` and the event system,
we can completely remove the tagging of DOM nodes.
2024-12-10 11:24:59 -05:00
Sebastian Markbåge
372ec00c03 Update ReactDebugInfo types to declare timing info separately (#31714)
This clarifies a few things by ensuring that there is always at least
one required field. This can be used to refine the object to one of the
specific types. However, it's probably just a matter of time until we
make this tagged unions instead. E.g. it would be nice to rename the
`name` field `ReactComponentInfo` to `type` and tag it with the React
Element symbol because then it's just the same as a React Element.

I also extract a time field. The idea is that this will advance (or
rewind) the time to the new timestamp and then anything below would be
defined as happening within that time stamp. E.g. to model the start and
end for a server component you'd do something like:

```
[
  {time: 123},
  {name: 'Component', ... },
  {time: 124},
]
```

The reason this needs to be in the `ReactDebugInfo` is so that timing
information from one environment gets transferred into the next
environment. It lets you take a Promise from one world and transfer it
into another world and its timing information is preserved without
everything else being preserved.

I've gone back and forth on if this should be part of each other Info
object like `ReactComponentInfo` but since those can be deduped and can
change formats (e.g. this should really just be a React Element) it's
better to store this separately.

The time format is relative to a `timeOrigin` which is the current
environment's `timeOrigin`. When it's serialized between environments
this needs to be considered.

Emitting these timings is not yet implemented in this PR.

---------

Co-authored-by: eps1lon <sebastian.silbermann@vercel.com>
2024-12-09 19:47:43 -05:00
Sebastian Markbåge
3d2ab01a55 [Flight] Extract special cases for Server Component return value position (#31713)
This is just moving some code into a helper.

We have a bunch of special cases for the return value slot of a Server
Component that's different from just rendering that inside an object.
This was getting a little tricky to reason about inline with the rest of
rendering.
2024-12-09 17:20:46 -05:00
Mike Vitousek
76d603a72a [compiler] Support for non-declatation for in/of iterators
ghstack-source-id: a28801e022
Pull Request resolved: https://github.com/facebook/react/pull/31710
2024-12-09 12:04:00 -08:00
Mike Vitousek
226b85926a [compiler] Support for context variable loop iterators
Summary:
Fixing a compiler todo

ghstack-source-id: c4d9226b17
Pull Request resolved: https://github.com/facebook/react/pull/31709
2024-12-09 12:03:52 -08:00
565 changed files with 37652 additions and 16031 deletions

View File

@@ -303,7 +303,6 @@ module.exports = {
ERROR,
{isProductionUserAppCode: true},
],
'react-internal/no-to-warn-dev-within-to-throw': ERROR,
'react-internal/warning-args': ERROR,
'react-internal/no-production-logging': ERROR,
},
@@ -330,6 +329,7 @@ module.exports = {
'packages/react-server-dom-esm/**/*.js',
'packages/react-server-dom-webpack/**/*.js',
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
@@ -481,6 +481,12 @@ module.exports = {
__turbopack_require__: 'readonly',
},
},
{
files: ['packages/react-server-dom-parcel/**/*.js'],
globals: {
parcelRequire: 'readonly',
},
},
{
files: ['packages/scheduler/**/*.js'],
globals: {

View File

@@ -9,7 +9,7 @@ on:
- .github/workflows/compiler_playground.yml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
@@ -38,11 +38,7 @@ jobs:
with:
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- name: yarn install compiler
run: yarn install --frozen-lockfile
working-directory: compiler
- name: yarn install playground
run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
- run: npx playwright install --with-deps chromium
- run: CI=true yarn test
- run: ls -R test-results

View File

@@ -16,7 +16,7 @@ on:
- compiler/*.toml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:

View File

@@ -9,7 +9,7 @@ on:
- .github/workflows/compiler_typescript.yml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:

View File

@@ -8,7 +8,7 @@ on:
- compiler/**
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
@@ -25,6 +25,8 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.result }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/github-script@v7
id: set-matrix
with:
@@ -42,6 +44,8 @@ jobs:
flow_inline_config_shortname: ${{ fromJSON(needs.discover_flow_inline_configs.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -64,6 +68,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -88,6 +94,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -139,6 +147,8 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -166,6 +176,8 @@ jobs:
release_channel: [stable, experimental]
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -242,6 +254,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -272,6 +286,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -317,6 +333,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -350,6 +368,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -380,6 +400,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -419,6 +441,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -480,6 +504,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -528,6 +554,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -562,6 +590,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -603,6 +633,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'

View File

@@ -0,0 +1,21 @@
name: (Shared) Discord Notify
on:
pull_request_target:
types: [labeled]
jobs:
notify:
if: ${{ github.event.label.name == 'React Core Team' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v6.0.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -6,7 +6,7 @@ on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:

View File

@@ -7,18 +7,18 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 19.0.0
// 19.1.0
//
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 19.0.0-canary-a1c2d3e4
// 19.1.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.0.0';
const ReactVersion = '19.1.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -26,27 +26,28 @@ const ReactVersion = '19.0.0';
//
// It only affects the label used in the version string. To customize the
// npm dist tags used during publish, refer to .github/workflows/runtime_prereleases_*.yml.
const canaryChannelLabel = 'rc';
const canaryChannelLabel = 'canary';
// If the canaryChannelLabel is "rc", the build pipeline will use this to build
// an RC version of the packages.
const rcNumber = 1;
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '5.1.0',
'jest-react': '0.16.0',
'eslint-plugin-react-hooks': '5.2.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,
'react-server-dom-webpack': ReactVersion,
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.31.0',
'react-refresh': '0.16.0',
'react-reconciler': '0.32.0',
'react-refresh': '0.17.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.10.0',
'use-sync-external-store': '1.4.0',
scheduler: '0.25.0',
'use-subscription': '1.11.0',
'use-sync-external-store': '1.5.0',
scheduler: '0.26.0',
};
// These packages do not exist in the @canary or @latest channel, only

View File

@@ -1,4 +1,5 @@
function TestComponent(t0) {
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;

View File

@@ -1,4 +1,5 @@
function MyApp() {
import { c as _c } from "react/compiler-runtime";
export default function MyApp() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {

View File

@@ -0,0 +1,13 @@
import { c as _c } from "react/compiler-runtime"; // 
        @compilationMode(all)
function nonReactFn() {
  const $ = _c(1);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = {};
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  return t0;
}

View File

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

View File

@@ -1,4 +1,6 @@
function TestComponent(t0) {
"use memo";
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;

View File

@@ -1,3 +1,4 @@
function TestComponent({ x }) {
"use no memo";
export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}

View File

@@ -0,0 +1,14 @@
import { c as _c } from "react/compiler-runtime";
function useFoo(propVal) {
  const $ = _c(2);
  const t0 = (propVal.baz: number);
  let t1;
  if ($[0] !== t0) {
    t1 = <div>{t0}</div>;
    $[0] = t0;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  return t1;
}

View File

@@ -0,0 +1,20 @@
import { c as _c } from "react/compiler-runtime";
function Foo() {
  const $ = _c(2);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = foo();
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  const x = t0 as number;
  let t1;
  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
    t1 = <div>{x}</div>;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  return t1;
}

View File

@@ -0,0 +1,5 @@
"use no memo";
function TestComponent({ x }) {
"use memo";
return <Button>{x}</Button>;
}

View File

@@ -1,3 +1,4 @@
import { c as _c } from "react/compiler-runtime";
function TestComponent(t0) {
"use memo";
const $ = _c(2);
@@ -12,7 +13,7 @@ function TestComponent(t0) {
}
return t1;
}
function anonymous_1(t0) {
const TestComponent2 = (t0) => {
"use memo";
const $ = _c(2);
const { x } = t0;
@@ -25,4 +26,4 @@ function anonymous_1(t0) {
t1 = $[1];
}
return t1;
}
};

View File

@@ -1,8 +1,8 @@
function anonymous_1() {
const TestComponent = function () {
"use no memo";
return <Button>{x}</Button>;
}
function anonymous_3({ x }) {
};
const TestComponent2 = ({ x }) => {
"use no memo";
return <Button>{x}</Button>;
}
};

View File

@@ -9,11 +9,11 @@ import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {format} from 'prettier';
function print(data: Array<string>): Promise<string> {
function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
const DIRECTIVE_TEST_CASES = [
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
input: `
@@ -55,7 +55,7 @@ const TestComponent2 = ({ x }) => {
};`,
},
{
name: 'function-scope-beats-module-scope',
name: 'todo-function-scope-does-not-beat-module-scope',
input: `
'use no memo';
function TestComponent({ x }) {
@@ -63,6 +63,44 @@ function TestComponent({ x }) {
return <Button>{x}</Button>;
}`,
},
{
name: 'parse-typescript',
input: `
function Foo() {
const x = foo() as number;
return <div>{x}</div>;
}
`,
noFormat: true,
},
{
name: 'parse-flow',
input: `
// @flow
function useFoo(propVal: {+baz: number}) {
return <div>{(propVal.baz as number)}</div>;
}
`,
noFormat: true,
},
{
name: 'compilationMode-infer',
input: `// @compilationMode(infer)
function nonReactFn() {
return {};
}
`,
noFormat: true,
},
{
name: 'compilationMode-all',
input: `// @compilationMode(all)
function nonReactFn() {
return {};
}
`,
noFormat: true,
},
];
test('editor should open successfully', async ({page}) => {
@@ -90,7 +128,7 @@ test('editor should compile from hash successfully', async ({page}) => {
});
const text =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await print(text);
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('01-user-output.txt');
@@ -115,14 +153,14 @@ test('reset button works', async ({page}) => {
});
const text =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await print(text);
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
});
DIRECTIVE_TEST_CASES.forEach((t, idx) =>
test(`directives work: ${t.name}`, async ({page}) => {
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
};
@@ -135,7 +173,12 @@ DIRECTIVE_TEST_CASES.forEach((t, idx) =>
const text =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await print(text);
let output: string;
if (t.noFormat) {
output = text.join('');
} else {
output = await formatPrint(text);
}
expect(output).not.toEqual('');
expect(output).toMatchSnapshot(`${t.name}-output.txt`);

View File

@@ -5,23 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse} from '@babel/parser';
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import traverse, {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
ValueKind,
runPlayground,
type Hook,
findDirectiveDisablingMemoization,
findDirectiveEnablingMemoization,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
} from 'babel-plugin-react-compiler/src';
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
@@ -39,32 +37,18 @@ import {useStore, useStoreDispatch} from '../StoreContext';
import Input from './Input';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
import {transformFromAstSync} from '@babel/core';
type FunctionLike =
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>;
enum MemoizeDirectiveState {
Enabled = 'Enabled',
Disabled = 'Disabled',
Undefined = 'Undefined',
}
const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
MemoizeDirectiveState.Enabled,
MemoizeDirectiveState.Undefined,
]);
const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
MemoizeDirectiveState.Enabled,
MemoizeDirectiveState.Disabled,
]);
function parseInput(input: string, language: 'flow' | 'typescript'): any {
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
@@ -77,95 +61,35 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
});
}) as ParseResult<t.File>;
}
}
function parseFunctions(
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
): Array<{
compilationEnabled: boolean;
fn: FunctionLike;
}> {
const items: Array<{
compilationEnabled: boolean;
fn: FunctionLike;
}> = [];
try {
const ast = parseInput(source, language);
traverse(ast, {
FunctionDeclaration(nodePath) {
items.push({
compilationEnabled: shouldCompile(nodePath),
fn: nodePath,
});
nodePath.skip();
},
ArrowFunctionExpression(nodePath) {
items.push({
compilationEnabled: shouldCompile(nodePath),
fn: nodePath,
});
nodePath.skip();
},
FunctionExpression(nodePath) {
items.push({
compilationEnabled: shouldCompile(nodePath),
fn: nodePath,
});
nodePath.skip();
},
});
} catch (e) {
console.error(e);
CompilerError.throwInvalidJS({
reason: String(e),
description: null,
loc: null,
suggestions: null,
});
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return items;
}
function shouldCompile(fn: FunctionLike): boolean {
const {body} = fn.node;
if (t.isBlockStatement(body)) {
const selfCheck = checkExplicitMemoizeDirectives(body.directives);
if (selfCheck === MemoizeDirectiveState.Enabled) return true;
if (selfCheck === MemoizeDirectiveState.Disabled) return false;
const parentWithDirective = fn.findParent(parentPath => {
if (parentPath.isBlockStatement() || parentPath.isProgram()) {
const directiveCheck = checkExplicitMemoizeDirectives(
parentPath.node.directives,
);
return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
}
return false;
});
if (!parentWithDirective) return true;
const parentDirectiveCheck = checkExplicitMemoizeDirectives(
(parentWithDirective.node as t.Program | t.BlockStatement).directives,
);
return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
}
return false;
}
function checkExplicitMemoizeDirectives(
directives: Array<t.Directive>,
): MemoizeDirectiveState {
if (findDirectiveEnablingMemoization(directives).length) {
return MemoizeDirectiveState.Enabled;
}
if (findDirectiveDisablingMemoization(directives).length) {
return MemoizeDirectiveState.Disabled;
}
return MemoizeDirectiveState.Undefined;
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
@@ -216,37 +140,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
],
];
function isHookName(s: string): boolean {
return /^use[A-Z0-9]/.test(s);
}
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
if (id != null) {
if (isHookName(id.name)) {
return 'Hook';
}
const isPascalCaseNameSpace = /^[A-Z].*/;
if (isPascalCaseNameSpace.test(id.name)) {
return 'Component';
}
}
return 'Other';
}
function getFunctionIdentifier(
fn:
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>,
): t.Identifier | null {
if (fn.isArrowFunctionExpression()) {
return null;
}
const id = fn.get('id');
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
}
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
@@ -264,101 +157,63 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
} else {
language = 'typescript';
}
let count = 0;
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
if (id != null && id.name != null) {
return id;
} else {
return t.identifier(`anonymous_${count++}`);
}
};
let transformOutput;
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const config = parseConfigPragmaForTests(pragma);
const parsedFunctions = parseFunctions(source, language);
for (const func of parsedFunctions) {
const id = withIdentifier(getFunctionIdentifier(func.fn));
const fnName = id.name;
if (!func.compilationEnabled) {
upsert({
kind: 'ast',
fnName,
name: 'CodeGen',
value: {
type: 'FunctionDeclaration',
id:
func.fn.isArrowFunctionExpression() ||
func.fn.isFunctionExpression()
? withIdentifier(null)
: func.fn.node.id,
async: func.fn.node.async,
generator: !!func.fn.node.generator,
body: func.fn.node.body as t.BlockStatement,
params: func.fn.node.params,
},
});
continue;
}
for (const result of runPlayground(
func.fn,
{
...config,
customHooks: new Map([...COMMON_HOOKS]),
},
getReactFunctionType(id),
)) {
switch (result.kind) {
case 'ast': {
upsert({
kind: 'ast',
fnName,
name: result.name,
value: {
type: 'FunctionDeclaration',
id: withIdentifier(result.value.id),
async: result.value.async,
generator: result.value.generator,
body: result.value.body,
params: result.value.params,
},
});
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
}
};
const parsedOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
});
const opts: PluginOptions = parsePluginOptions({
...parsedOptions,
environment: {
...parsedOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
logger: {
debugLogIRs: logIR,
logEvent: () => {},
},
});
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
@@ -385,7 +240,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}
return [{kind: 'ok', results}, language];
return [{kind: 'ok', results, transformOutput}, language];
}
export default function Editor(): JSX.Element {
@@ -405,7 +260,7 @@ export default function Editor(): JSX.Element {
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
enqueueSnackbar(e.message, {
variant: 'message',
variant: 'warning',
...createMessage(
'Bad URL - fell back to the default Playground.',
MessageLevel.Info,

View File

@@ -5,8 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import * as t from '@babel/types';
import {
CodeIcon,
DocumentAddIcon,
@@ -21,17 +19,12 @@ import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export type PrintedCompilerPipelineValue =
| {
kind: 'ast';
name: string;
fnName: string | null;
value: t.FunctionDeclaration;
}
| {
kind: 'hir';
name: string;
@@ -41,8 +34,17 @@ export type PrintedCompilerPipelineValue =
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
| {kind: 'debug'; name: string; fnName: string | null; value: string};
export type CompilerTransformOutput = {
code: string;
sourceMaps: BabelFileResult['map'];
language: 'flow' | 'typescript';
};
export type CompilerOutput =
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
| {
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
}
| {
kind: 'err';
results: Map<string, Array<PrintedCompilerPipelineValue>>;
@@ -61,7 +63,6 @@ async function tabify(
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -87,9 +88,6 @@ async function tabify(
}
break;
}
case 'ast':
topLevelFnDecls.push(result.value);
break;
case 'debug': {
concattedResults.set(passName, result.value);
break;
@@ -114,13 +112,17 @@ async function tabify(
lastPassOutput = text;
}
// Ensure that JS and the JS source map come first
if (topLevelFnDecls.length > 0) {
/**
* Make a synthetic Program so we can have a single AST with all the top level
* FunctionDeclarations
*/
const ast = t.program(topLevelFnDecls);
const {code, sourceMapUrl} = await codegen(ast, source);
if (compilerOutput.kind === 'ok') {
const {transformOutput} = compilerOutput;
const sourceMapUrl = getSourceMapUrl(
transformOutput.code,
JSON.stringify(transformOutput.sourceMaps),
);
const code = await prettier.format(transformOutput.code, {
semi: true,
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
reorderedTabs.set(
'JS',
<TextTabContent
@@ -147,27 +149,6 @@ async function tabify(
return reorderedTabs;
}
async function codegen(
ast: t.Program,
source: string,
): Promise<{code: any; sourceMapUrl: string | null}> {
const generated = generate(
ast,
{sourceMaps: true, sourceFileName: 'input.js'},
source,
);
const sourceMapUrl = getSourceMapUrl(
generated.code,
JSON.stringify(generated.map),
);
const codegenOutput = await prettier.format(generated.code, {
semi: true,
parser: 'babel',
plugins: [parserBabel, prettierPluginEstree],
});
return {code: codegenOutput, sourceMapUrl};
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}

View File

@@ -3,10 +3,11 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run watch\" \"yarn workspace react-compiler-runtime run watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
"build": "yarn build:compiler && next build",
"postbuild": "node ./scripts/downloadFonts.js",
"preinstall": "cd ../.. && yarn install --frozen-lockfile",
"postinstall": "./scripts/link-compiler.sh",
"vercel-build": "yarn build",
"start": "next start",

View File

@@ -15,7 +15,7 @@
"start": "yarn workspace playground run start",
"next": "yarn workspace playground run dev",
"build": "yarn workspaces run build",
"dev": "echo 'DEPRECATED: use `cd apps/playground && yarn dev` instead!' && sleep 5 && cd apps/playground && yarn dev",
"dev": "cd apps/playground && yarn dev",
"test": "yarn workspaces run test",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
@@ -26,25 +26,23 @@
"react-is": "0.0.0-experimental-4beb1fd8-20241118"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@tsconfig/strictest": "^2.0.5",
"concurrently": "^7.4.0",
"esbuild": "^0.24.2",
"folder-hash": "^4.0.4",
"npm-dts": "^1.3.13",
"object-assign": "^4.1.1",
"ora": "5.4.1",
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.25.1",
"prettier-plugin-hermes-parser": "^0.26.0",
"prompt-promise": "^1.0.3",
"rollup": "^4.22.4",
"rollup-plugin-banner2": "^1.2.3",
"rollup-plugin-prettier": "^4.1.1",
"rimraf": "^5.0.10",
"typescript": "^5.4.3",
"wait-on": "^7.2.0",
"yargs": "^17.7.2"
},
"resolutions": {
"rimraf": "5.0.10"
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -9,14 +9,15 @@
"!*.tsbuildinfo"
],
"scripts": {
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
"build": "rimraf dist && scripts/build.js",
"test": "./scripts/link-react-compiler-runtime.sh && yarn snap:ci",
"jest": "yarn build && ts-node node_modules/.bin/jest",
"snap": "node ../snap/dist/main.js",
"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": "yarn eslint src",
"watch": "scripts/build.js --watch"
},
"dependencies": {
"@babel/types": "^7.19.0"
@@ -42,16 +43,13 @@
"babel-jest": "^29.0.3",
"babel-plugin-fbt": "^1.0.0",
"babel-plugin-fbt-runtime": "^1.0.0",
"chalk": "4",
"eslint": "^8.57.1",
"glob": "^7.1.6",
"invariant": "^2.2.4",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"pretty-format": "^24",
"react": "0.0.0-experimental-4beb1fd8-20241118",
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"zod": "^3.22.4",

View File

@@ -1,71 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import typescript from '@rollup/plugin-typescript';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import path from 'path';
import process from 'process';
import terser from '@rollup/plugin-terser';
import prettier from 'rollup-plugin-prettier';
import banner2 from 'rollup-plugin-banner2';
const NO_INLINE = new Set(['@babel/types']);
const DEV_ROLLUP_CONFIG = {
input: 'src/index.ts',
output: {
file: 'dist/index.js',
format: 'cjs',
sourcemap: false,
exports: 'named',
},
plugins: [
typescript({
tsconfig: './tsconfig.json',
outputToFilesystem: true,
compilerOptions: {
noEmit: true,
},
}),
json(),
nodeResolve({
preferBuiltins: true,
resolveOnly: module => NO_INLINE.has(module) === false,
rootDir: path.join(process.cwd(), '..'),
}),
commonjs(),
terser({
format: {
comments: false,
},
compress: false,
mangle: false,
}),
prettier(),
banner2(
() => `/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/
"use no memo";
`
),
],
};
export default DEV_ROLLUP_CONFIG;

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const esbuild = require('esbuild');
const yargs = require('yargs');
const path = require('path');
const argv = yargs(process.argv.slice(2))
.options('w', {
alias: 'watch',
default: false,
type: 'boolean',
})
.parse();
const config = {
entryPoints: [path.join(__dirname, '../src/index.ts')],
outfile: path.join(__dirname, '../dist/index.js'),
bundle: true,
external: ['@babel/types'],
format: 'cjs',
platform: 'node',
banner: {
js: `/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/
"use no memo";`,
},
};
async function main() {
if (argv.w) {
const ctx = await esbuild.context(config);
await ctx.watch();
console.log('watching for changes...');
} else {
await esbuild.build({
sourcemap: true,
minify: false,
...config,
});
}
}
main();

View File

@@ -39,7 +39,10 @@ export default function BabelPluginReactCompiler(
) {
opts = injectReanimatedFlag(opts);
}
if (isDev) {
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {

View File

@@ -15,6 +15,7 @@ import {
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([
/*
@@ -209,6 +210,7 @@ export type LoggerEvent =
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: PluginOptions = {

View File

@@ -79,13 +79,6 @@ import {
rewriteInstructionKindsBasedOnReassignment,
} from '../SSA';
import {inferTypes} from '../TypeInference';
import {
logCodegenFunction,
logDebug,
logHIRFunction,
logReactiveFunction,
} from '../Utils/logger';
import {assertExhaustive} from '../Utils/utils';
import {
validateContextVariableLValues,
validateHooksUsage,
@@ -104,6 +97,8 @@ import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetSta
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -111,7 +106,7 @@ export type CompilerPipelineValue =
| {kind: 'reactive'; name: string; value: ReactiveFunction}
| {kind: 'debug'; name: string; value: string};
export function* run(
function run(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
@@ -121,7 +116,7 @@ export function* run(
logger: Logger | null,
filename: string | null,
code: string | null,
): Generator<CompilerPipelineValue, CodegenFunction> {
): CodegenFunction {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
func.scope,
@@ -133,30 +128,32 @@ export function* run(
code,
useMemoCacheIdentifier,
);
yield log({
env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'EnvironmentConfig',
value: prettyFormat(env.config),
});
const ast = yield* runWithEnvironment(func, env);
return ast;
return runWithEnvironment(func, env);
}
/*
* Note: this is split from run() to make `config` out of scope, so that all
* access to feature flags has to be through the Environment for consistency.
*/
function* runWithEnvironment(
function runWithEnvironment(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
env: Environment,
): Generator<CompilerPipelineValue, CodegenFunction> {
): CodegenFunction {
const log = (value: CompilerPipelineValue): void => {
env.logger?.debugLogIRs?.(value);
};
const hir = lower(func, env).unwrap();
yield log({kind: 'hir', name: 'HIR', value: hir});
log({kind: 'hir', name: 'HIR', value: hir});
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir);
@@ -167,40 +164,45 @@ function* runWithEnvironment(
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
inlineImmediatelyInvokedFunctionExpressions(hir);
yield log({
log({
kind: 'hir',
name: 'InlineImmediatelyInvokedFunctionExpressions',
value: hir,
});
mergeConsecutiveBlocks(hir);
yield log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
assertConsistentIdentifiers(hir);
assertTerminalSuccessorsExist(hir);
enterSSA(hir);
yield log({kind: 'hir', name: 'SSA', value: hir});
log({kind: 'hir', name: 'SSA', value: hir});
eliminateRedundantPhi(hir);
yield log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
assertConsistentIdentifiers(hir);
constantPropagation(hir);
yield log({kind: 'hir', name: 'ConstantPropagation', value: hir});
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
inferTypes(hir);
yield log({kind: 'hir', name: 'InferTypes', value: hir});
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
}
if (env.config.enableFire) {
transformFire(hir);
log({kind: 'hir', name: 'TransformFire', value: hir});
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
}
@@ -209,28 +211,31 @@ function* runWithEnvironment(
lowerContextAccess(hir, env.config.lowerContextAccess);
}
optimizePropsMethodCalls(hir);
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
analyseFunctions(hir);
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferReferenceEffects(hir);
yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
yield log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
if (env.config.enableInstructionReordering) {
instructionReordering(hir);
yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
log({kind: 'hir', name: 'InstructionReordering', value: hir});
}
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutableRanges(hir);
yield log({kind: 'hir', name: 'InferMutableRanges', value: hir});
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
@@ -253,27 +258,27 @@ function* runWithEnvironment(
}
inferReactivePlaces(hir);
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
rewriteInstructionKindsBasedOnReassignment(hir);
yield log({
log({
kind: 'hir',
name: 'RewriteInstructionKindsBasedOnReassignment',
value: hir,
});
propagatePhiTypes(hir);
yield log({
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
inferReactiveScopeVariables(hir);
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
yield log({
log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
value: hir,
@@ -285,39 +290,39 @@ function* runWithEnvironment(
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
}
alignMethodCallScopes(hir);
yield log({
log({
kind: 'hir',
name: 'AlignMethodCallScopes',
value: hir,
});
alignObjectMethodScopes(hir);
yield log({
log({
kind: 'hir',
name: 'AlignObjectMethodScopes',
value: hir,
});
pruneUnusedLabelsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
value: hir,
@@ -325,7 +330,7 @@ function* runWithEnvironment(
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
@@ -334,14 +339,14 @@ function* runWithEnvironment(
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
yield log({
log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
value: hir,
@@ -349,7 +354,7 @@ function* runWithEnvironment(
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'PropagateScopeDependenciesHIR',
value: hir,
@@ -361,7 +366,7 @@ function* runWithEnvironment(
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
yield log({
log({
kind: 'hir',
name: 'inlineJsxTransform',
value: hir,
@@ -369,7 +374,7 @@ function* runWithEnvironment(
}
const reactiveFunction = buildReactiveFunction(hir);
yield log({
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
value: reactiveFunction,
@@ -378,7 +383,7 @@ function* runWithEnvironment(
assertWellFormedBreakTargets(reactiveFunction);
pruneUnusedLabels(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedLabels',
value: reactiveFunction,
@@ -386,35 +391,35 @@ function* runWithEnvironment(
assertScopeInstructionsWithinScopes(reactiveFunction);
pruneNonEscapingScopes(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneNonEscapingScopes',
value: reactiveFunction,
});
pruneNonReactiveDependencies(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneNonReactiveDependencies',
value: reactiveFunction,
});
pruneUnusedScopes(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedScopes',
value: reactiveFunction,
});
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'MergeReactiveScopesThatInvalidateTogether',
value: reactiveFunction,
});
pruneAlwaysInvalidatingScopes(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneAlwaysInvalidatingScopes',
value: reactiveFunction,
@@ -422,7 +427,7 @@ function* runWithEnvironment(
if (env.config.enableChangeDetectionForDebugging != null) {
pruneInitializationDependencies(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneInitializationDependencies',
value: reactiveFunction,
@@ -430,49 +435,49 @@ function* runWithEnvironment(
}
propagateEarlyReturns(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PropagateEarlyReturns',
value: reactiveFunction,
});
pruneUnusedLValues(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedLValues',
value: reactiveFunction,
});
promoteUsedTemporaries(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PromoteUsedTemporaries',
value: reactiveFunction,
});
extractScopeDeclarationsFromDestructuring(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'ExtractScopeDeclarationsFromDestructuring',
value: reactiveFunction,
});
stabilizeBlockIds(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'StabilizeBlockIds',
value: reactiveFunction,
});
const uniqueIdentifiers = renameVariables(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'RenameVariables',
value: reactiveFunction,
});
pruneHoistedContexts(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneHoistedContexts',
value: reactiveFunction,
@@ -493,9 +498,9 @@ function* runWithEnvironment(
uniqueIdentifiers,
fbtOperands,
}).unwrap();
yield log({kind: 'ast', name: 'Codegen', value: ast});
log({kind: 'ast', name: 'Codegen', value: ast});
for (const outlined of ast.outlined) {
yield log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
/**
@@ -521,7 +526,7 @@ export function compileFn(
filename: string | null,
code: string | null,
): CodegenFunction {
let generator = run(
return run(
func,
config,
fnType,
@@ -530,46 +535,4 @@ export function compileFn(
filename,
code,
);
while (true) {
const next = generator.next();
if (next.done) {
return next.value;
}
}
}
export function log(value: CompilerPipelineValue): CompilerPipelineValue {
switch (value.kind) {
case 'ast': {
logCodegenFunction(value.name, value.value);
break;
}
case 'hir': {
logHIRFunction(value.name, value.value);
break;
}
case 'reactive': {
logReactiveFunction(value.name, value.value);
break;
}
case 'debug': {
logDebug(value.name, value.value);
break;
}
default: {
assertExhaustive(value, 'Unexpected compilation kind');
}
}
return value;
}
export function* runPlayground(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
): Generator<CompilerPipelineValue, CodegenFunction> {
const ast = yield* run(func, config, fnType, '_c', null, null, null);
return ast;
}

View File

@@ -564,6 +564,14 @@ export function compileProgram(
if (environment.enableChangeDetectionForDebugging != null) {
externalFunctions.push(environment.enableChangeDetectionForDebugging);
}
const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite);
if (environment.enableFire && hasFireRewrite) {
externalFunctions.push({
source: getReactCompilerRuntimeModule(pass.opts),
importSpecifierName: 'useFire',
});
}
} catch (err) {
handleError(err, pass, null);
return;

View File

@@ -1078,6 +1078,12 @@ function lowerStatement(
const left = stmt.get('left');
const leftLoc = left.node.loc ?? GeneratedSource;
let test: Place;
const advanceIterator = lowerValueToTemporary(builder, {
kind: 'IteratorNext',
loc: leftLoc,
iterator: {...iterator},
collection: {...value},
});
if (left.isVariableDeclaration()) {
const declarations = left.get('declarations');
CompilerError.invariant(declarations.length === 1, {
@@ -1087,12 +1093,6 @@ function lowerStatement(
suggestions: null,
});
const id = declarations[0].get('id');
const advanceIterator = lowerValueToTemporary(builder, {
kind: 'IteratorNext',
loc: leftLoc,
iterator: {...iterator},
collection: {...value},
});
const assign = lowerAssignment(
builder,
leftLoc,
@@ -1103,13 +1103,19 @@ function lowerStatement(
);
test = lowerValueToTemporary(builder, assign);
} else {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForOfStatement`,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
CompilerError.invariant(left.isLVal(), {
loc: leftLoc,
reason: 'Expected ForOf init to be a variable declaration or lval',
});
return;
const assign = lowerAssignment(
builder,
leftLoc,
InstructionKind.Reassign,
left,
advanceIterator,
'Assignment',
);
test = lowerValueToTemporary(builder, assign);
}
builder.terminateWithContinuation(
{
@@ -1166,6 +1172,11 @@ function lowerStatement(
const left = stmt.get('left');
const leftLoc = left.node.loc ?? GeneratedSource;
let test: Place;
const nextPropertyTemp = lowerValueToTemporary(builder, {
kind: 'NextPropertyOf',
loc: leftLoc,
value,
});
if (left.isVariableDeclaration()) {
const declarations = left.get('declarations');
CompilerError.invariant(declarations.length === 1, {
@@ -1175,11 +1186,6 @@ function lowerStatement(
suggestions: null,
});
const id = declarations[0].get('id');
const nextPropertyTemp = lowerValueToTemporary(builder, {
kind: 'NextPropertyOf',
loc: leftLoc,
value,
});
const assign = lowerAssignment(
builder,
leftLoc,
@@ -1190,13 +1196,19 @@ function lowerStatement(
);
test = lowerValueToTemporary(builder, assign);
} else {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForInStatement`,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
CompilerError.invariant(left.isLVal(), {
loc: leftLoc,
reason: 'Expected ForIn init to be a variable declaration or lval',
});
return;
const assign = lowerAssignment(
builder,
leftLoc,
InstructionKind.Reassign,
left,
nextPropertyTemp,
'Assignment',
);
test = lowerValueToTemporary(builder, assign);
}
builder.terminateWithContinuation(
{

View File

@@ -9,7 +9,13 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {Logger} from '../Entrypoint';
import {
CompilationMode,
Logger,
PanicThresholdOptions,
parsePluginOptions,
PluginOptions,
} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -168,11 +174,19 @@ const EnvironmentConfigSchema = z.object({
customMacros: z.nullable(z.array(MacroSchema)).default(null),
/**
* Enable a check that resets the memoization cache when the source code of the file changes.
* This is intended to support hot module reloading (HMR), where the same runtime component
* instance will be reused across different versions of the component source.
* Enable a check that resets the memoization cache when the source code of
* the file changes. This is intended to support hot module reloading (HMR),
* where the same runtime component instance will be reused across different
* versions of the component source.
*
* When set to
* - true: code for HMR support is always generated, regardless of NODE_ENV
* or `globalThis.__DEV__`
* - false: code for HMR support is not generated
* - null: (default) code for HMR support is conditionally generated dependent
* on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.
*/
enableResetCacheOnSourceFileChanges: z.boolean().default(false),
enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),
/**
* Enable using information from existing useMemo/useCallback to understand when a value is done
@@ -241,6 +255,8 @@ const EnvironmentConfigSchema = z.object({
*/
enableOptionalDependencies: z.boolean().default(true),
enableFire: z.boolean().default(false),
/**
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
* configurable module and import pairs to allow for user-land experimentation. For example,
@@ -673,7 +689,9 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
/**
* For snap test fixtures and playground only.
*/
export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
function parseConfigPragmaEnvironmentForTest(
pragma: string,
): EnvironmentConfig {
const maybeConfig: any = {};
// Get the defaults to programmatically check for boolean properties
const defaultConfig = EnvironmentConfigSchema.parse({});
@@ -708,7 +726,10 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
continue;
}
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
if (
key !== 'enableResetCacheOnSourceFileChanges' &&
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
) {
// skip parsing non-boolean properties
continue;
}
@@ -718,9 +739,15 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
maybeConfig[key] = false;
}
}
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
if (config.success) {
/**
* Unless explicitly enabled, do not insert HMR handling code
* in test fixtures or playground to reduce visual noise.
*/
if (config.data.enableResetCacheOnSourceFileChanges == null) {
config.data.enableResetCacheOnSourceFileChanges = false;
}
return config.data;
}
CompilerError.invariant(false, {
@@ -730,6 +757,48 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
suggestions: null,
});
}
export function parseConfigPragmaForTests(
pragma: string,
defaults: {
compilationMode: CompilationMode;
},
): PluginOptions {
const environment = parseConfigPragmaEnvironmentForTest(pragma);
let compilationMode: CompilationMode = defaults.compilationMode;
let panicThreshold: PanicThresholdOptions = 'all_errors';
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
switch (token) {
case '@compilationMode(annotation)': {
compilationMode = 'annotation';
break;
}
case '@compilationMode(infer)': {
compilationMode = 'infer';
break;
}
case '@compilationMode(all)': {
compilationMode = 'all';
break;
}
case '@compilationMode(syntax)': {
compilationMode = 'syntax';
break;
}
case '@panicThreshold(none)': {
panicThreshold = 'none';
break;
}
}
}
return parsePluginOptions({
environment,
compilationMode,
panicThreshold,
});
}
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
@@ -768,6 +837,7 @@ export class Environment {
fnType: ReactFunctionType;
useMemoCacheIdentifier: string;
hasLoweredContextAccess: boolean;
hasFireRewrite: boolean;
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
@@ -792,6 +862,7 @@ export class Environment {
this.#shapes = new Map(DEFAULT_SHAPES);
this.#globals = new Map(DEFAULT_GLOBALS);
this.hasLoweredContextAccess = false;
this.hasFireRewrite = false;
if (
config.disableMemoizationForDebugging &&

View File

@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInFireId,
BuiltInMixedReadonlyId,
BuiltInUseActionStateId,
BuiltInUseContextHookId,
@@ -87,6 +88,21 @@ const UNTYPED_GLOBALS: Set<string> = new Set([
]);
const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
[
'Object',
addObject(DEFAULT_SHAPES, 'Object', [
[
'keys',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
}),
],
]),
],
[
'Array',
addObject(DEFAULT_SHAPES, 'Array', [
@@ -468,6 +484,21 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInUseOperatorId,
),
],
[
'fire',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [],
restParam: null,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
},
BuiltInFireId,
),
],
];
TYPED_GLOBALS.push(

View File

@@ -840,6 +840,11 @@ export type LoadLocal = {
place: Place;
loc: SourceLocation;
};
export type LoadContext = {
kind: 'LoadContext';
place: Place;
loc: SourceLocation;
};
/*
* The value of a given instruction. Note that values are not recursive: complex
@@ -852,11 +857,7 @@ export type LoadLocal = {
export type InstructionValue =
| LoadLocal
| {
kind: 'LoadContext';
place: Place;
loc: SourceLocation;
}
| LoadContext
| {
kind: 'DeclareLocal';
lvalue: LValue;
@@ -1644,6 +1645,10 @@ export function isArrayType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
}
export function isPropsType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInProps';
}
export function isRefValueType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInRefValue';
}

View File

@@ -213,6 +213,7 @@ export const BuiltInDispatchId = 'BuiltInDispatch';
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
// ShapeRegistry with default definitions for built-ins.
export const BUILTIN_SHAPES: ShapeRegistry = new Map();

View File

@@ -897,6 +897,14 @@ export function printSourceLocation(loc: SourceLocation): string {
}
}
export function printSourceLocationLine(loc: SourceLocation): string {
if (typeof loc === 'symbol') {
return 'generated';
} else {
return `${loc.start.line}:${loc.end.line}`;
}
}
export function printAliases(aliases: DisjointSet<Identifier>): string {
const aliasSets = aliases.buildSets();

View File

@@ -17,6 +17,11 @@ import {
areEqualPaths,
IdentifierId,
Terminal,
InstructionValue,
LoadContext,
TInstruction,
FunctionExpression,
ObjectMethod,
} from './HIR';
import {
collectHoistablePropertyLoads,
@@ -223,11 +228,25 @@ export function collectTemporariesSidemap(
fn,
usedOutsideDeclaringScope,
temporaries,
false,
null,
);
return temporaries;
}
function isLoadContextMutable(
instrValue: InstructionValue,
id: InstructionId,
): instrValue is LoadContext {
if (instrValue.kind === 'LoadContext') {
CompilerError.invariant(instrValue.place.identifier.scope != null, {
reason:
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
loc: instrValue.loc,
});
return id >= instrValue.place.identifier.scope.range.end;
}
return false;
}
/**
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
* function and all nested functions.
@@ -239,17 +258,21 @@ function collectTemporariesSidemapImpl(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
isInnerFn: boolean,
innerFnContext: {instrId: InstructionId} | null,
): void {
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
for (const {value, lvalue, id: origInstrId} of block.instructions) {
const instrId =
innerFnContext != null ? innerFnContext.instrId : origInstrId;
const usedOutside = usedOutsideDeclaringScope.has(
lvalue.identifier.declarationId,
);
if (value.kind === 'PropertyLoad' && !usedOutside) {
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
if (
innerFnContext == null ||
temporaries.has(value.object.identifier.id)
) {
/**
* All dependencies of a inner / nested function must have a base
* identifier from the outermost component / hook. This is because the
@@ -265,13 +288,13 @@ function collectTemporariesSidemapImpl(
temporaries.set(lvalue.identifier.id, property);
}
} else if (
value.kind === 'LoadLocal' &&
(value.kind === 'LoadLocal' || isLoadContextMutable(value, instrId)) &&
lvalue.identifier.name == null &&
value.place.identifier.name !== null &&
!usedOutside
) {
if (
!isInnerFn ||
innerFnContext == null ||
fn.context.some(
context => context.identifier.id === value.place.identifier.id,
)
@@ -289,7 +312,7 @@ function collectTemporariesSidemapImpl(
value.loweredFunc.func,
usedOutsideDeclaringScope,
temporaries,
true,
innerFnContext ?? {instrId},
);
}
}
@@ -358,19 +381,22 @@ class Context {
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
#processedInstrsInOptional: ReadonlySet<Instruction | Terminal>;
/**
* Tracks the traversal state. See Context.declare for explanation of why this
* is needed.
*/
inInnerFn: boolean = false;
#innerFnContext: {outerInstrId: InstructionId} | null = null;
constructor(
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
) {
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
this.#temporaries = temporaries;
this.#processedInstrsInOptional = processedInstrsInOptional;
}
enterScope(scope: ReactiveScope): void {
@@ -431,7 +457,7 @@ class Context {
* by root identifier mutable ranges).
*/
declare(identifier: Identifier, decl: Decl): void {
if (this.inInnerFn) return;
if (this.#innerFnContext != null) return;
if (!this.#declarations.has(identifier.declarationId)) {
this.#declarations.set(identifier.declarationId, decl);
}
@@ -574,22 +600,52 @@ class Context {
currentScope.reassignments.add(place.identifier);
}
}
enterInnerFn<T>(
innerFn: TInstruction<FunctionExpression> | TInstruction<ObjectMethod>,
cb: () => T,
): T {
const prevContext = this.#innerFnContext;
this.#innerFnContext = this.#innerFnContext ?? {outerInstrId: innerFn.id};
const result = cb();
this.#innerFnContext = prevContext;
return result;
}
/**
* Skip dependencies that are subexpressions of other dependencies. e.g. if a
* dependency is tracked in the temporaries sidemap, it can be added at
* site-of-use
*/
isDeferredDependency(
instr:
| {kind: HIRValue.Instruction; value: Instruction}
| {kind: HIRValue.Terminal; value: Terminal},
): boolean {
return (
this.#processedInstrsInOptional.has(instr.value) ||
(instr.kind === HIRValue.Instruction &&
this.#temporaries.has(instr.value.lvalue.identifier.id))
);
}
}
enum HIRValue {
Instruction = 1,
Terminal,
}
function handleInstruction(instr: Instruction, context: Context): void {
const {id, value, lvalue} = instr;
if (value.kind === 'LoadLocal') {
if (
value.place.identifier.name === null ||
lvalue.identifier.name !== null ||
context.isUsedOutsideDeclaringScope(lvalue)
) {
context.visitOperand(value.place);
}
} else if (value.kind === 'PropertyLoad') {
if (context.isUsedOutsideDeclaringScope(lvalue)) {
context.visitProperty(value.object, value.property, false);
}
context.declare(lvalue.identifier, {
id,
scope: context.currentScope,
});
if (
context.isDeferredDependency({kind: HIRValue.Instruction, value: instr})
) {
return;
}
if (value.kind === 'PropertyLoad') {
context.visitProperty(value.object, value.property, false);
} else if (value.kind === 'StoreLocal') {
context.visitOperand(value.value);
if (value.lvalue.kind === InstructionKind.Reassign) {
@@ -632,11 +688,6 @@ function handleInstruction(instr: Instruction, context: Context): void {
context.visitOperand(operand);
}
}
context.declare(lvalue.identifier, {
id,
scope: context.currentScope,
});
}
function collectDependencies(
@@ -645,7 +696,11 @@ function collectDependencies(
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new Context(usedOutsideDeclaringScope, temporaries);
const context = new Context(
usedOutsideDeclaringScope,
temporaries,
processedInstrsInOptional,
);
for (const param of fn.params) {
if (param.kind === 'Identifier') {
@@ -694,16 +749,26 @@ function collectDependencies(
/**
* Recursively visit the inner function to extract dependencies there
*/
const wasInInnerFn = context.inInnerFn;
context.inInnerFn = true;
handleFunction(instr.value.loweredFunc.func);
context.inInnerFn = wasInInnerFn;
} else if (!processedInstrsInOptional.has(instr)) {
const innerFn = instr.value.loweredFunc.func;
context.enterInnerFn(
instr as
| TInstruction<FunctionExpression>
| TInstruction<ObjectMethod>,
() => {
handleFunction(innerFn);
},
);
} else {
handleInstruction(instr, context);
}
}
if (!processedInstrsInOptional.has(block.terminal)) {
if (
!context.isDeferredDependency({
kind: HIRValue.Terminal,
value: block.terminal,
})
) {
for (const place of eachTerminalOperand(block.terminal)) {
context.visitOperand(place);
}

View File

@@ -19,7 +19,6 @@ import {
import {deadCodeElimination} from '../Optimization';
import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {logHIRFunction} from '../Utils/logger';
import {inferMutableContextVariables} from './InferMutableContextVariables';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
@@ -112,7 +111,11 @@ function lower(func: HIRFunction): void {
rewriteInstructionKindsBasedOnReassignment(func);
inferReactiveScopeVariables(func);
inferMutableContextVariables(func);
logHIRFunction('AnalyseFunction (inner)', func);
func.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: func,
});
}
function infer(

View File

@@ -546,16 +546,14 @@ function createPropsProperties(
let refProperty: ObjectProperty | undefined;
let keyProperty: ObjectProperty | undefined;
const props: Array<ObjectProperty | SpreadPattern> = [];
const jsxAttributesWithoutKeyAndRef = propAttributes.filter(
p => p.kind === 'JsxAttribute' && p.name !== 'key' && p.name !== 'ref',
const jsxAttributesWithoutKey = propAttributes.filter(
p => p.kind === 'JsxAttribute' && p.name !== 'key',
);
const jsxSpreadAttributes = propAttributes.filter(
p => p.kind === 'JsxSpreadAttribute',
);
const spreadPropsOnly =
jsxAttributesWithoutKeyAndRef.length === 0 &&
jsxSpreadAttributes.length === 1;
jsxAttributesWithoutKey.length === 0 && jsxSpreadAttributes.length === 1;
propAttributes.forEach(prop => {
switch (prop.kind) {
case 'JsxAttribute': {

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, isPropsType} from '../HIR';
/**
* Converts method calls into regular calls where the receiver is the props object:
*
* Example:
*
* ```
* // INPUT
* props.foo();
*
* // OUTPUT
* const t0 = props.foo;
* t0();
* ```
*
* Counter example:
*
* Here the receiver is `props.foo`, not the props object, so we don't rewrite it:
*
* // INPUT
* props.foo.bar();
*
* // OUTPUT
* props.foo.bar();
* ```
*/
export function optimizePropsMethodCalls(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i]!;
if (
instr.value.kind === 'MethodCall' &&
isPropsType(instr.value.receiver.identifier)
) {
instr.value = {
kind: 'CallExpression',
callee: instr.value.property,
args: instr.value.args,
loc: instr.value.loc,
};
}
}
}
}

View File

@@ -103,6 +103,11 @@ export type CodegenFunction = {
* This is true if the compiler has the lowered useContext calls.
*/
hasLoweredContextAccess: boolean;
/**
* This is true if the compiler has compiled a fire to a useFire call
*/
hasFireRewrite: boolean;
};
export function codegenFunction(
@@ -355,6 +360,7 @@ function codegenReactiveFunction(
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
outlined: [],
hasLoweredContextAccess: fn.env.hasLoweredContextAccess,
hasFireRewrite: fn.env.hasFireRewrite,
});
}
@@ -1354,20 +1360,6 @@ function codegenForInit(
init: ReactiveValue,
): t.Expression | t.VariableDeclaration | null {
if (init.kind === 'SequenceExpression') {
for (const instr of init.instructions) {
if (instr.value.kind === 'DeclareContext') {
CompilerError.throwTodo({
reason: `Support for loops where the index variable is a context variable`,
loc: instr.loc,
description:
instr.value.lvalue.place.identifier.name != null
? `\`${instr.value.lvalue.place.identifier.name.value}\` is a context variable`
: null,
suggestions: null,
});
}
}
const body = codegenBlock(
cx,
init.instructions.map(instruction => ({
@@ -1378,20 +1370,33 @@ function codegenForInit(
const declarators: Array<t.VariableDeclarator> = [];
let kind: 'let' | 'const' = 'const';
body.forEach(instr => {
CompilerError.invariant(
instr.type === 'VariableDeclaration' &&
(instr.kind === 'let' || instr.kind === 'const'),
{
reason: 'Expected a variable declaration',
loc: init.loc,
description: `Got ${instr.type}`,
suggestions: null,
},
);
if (instr.kind === 'let') {
kind = 'let';
let top: undefined | t.VariableDeclarator = undefined;
if (
instr.type === 'ExpressionStatement' &&
instr.expression.type === 'AssignmentExpression' &&
instr.expression.operator === '=' &&
instr.expression.left.type === 'Identifier' &&
(top = declarators.at(-1))?.id.type === 'Identifier' &&
top?.id.name === instr.expression.left.name &&
top?.init == null
) {
top.init = instr.expression.right;
} else {
CompilerError.invariant(
instr.type === 'VariableDeclaration' &&
(instr.kind === 'let' || instr.kind === 'const'),
{
reason: 'Expected a variable declaration',
loc: init.loc,
description: `Got ${instr.type}`,
suggestions: null,
},
);
if (instr.kind === 'let') {
kind = 'let';
}
declarators.push(...instr.declarations);
}
declarators.push(...instr.declarations);
});
CompilerError.invariant(declarators.length > 0, {
reason: 'Expected a variable declaration',

View File

@@ -25,7 +25,6 @@ import {
eachPatternOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {logHIRFunction} from '../Utils/logger';
import {assertExhaustive} from '../Utils/utils';
/*
@@ -156,7 +155,11 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
scope.range.end > maxInstruction + 1
) {
// Make it easier to debug why the error occurred
logHIRFunction('InferReactiveScopeVariables (invalid scope)', fn);
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'InferReactiveScopeVariables (invalid scope)',
value: fn,
});
CompilerError.invariant(false, {
reason: `Invalid mutable range for scope`,
loc: GeneratedSource,

View File

@@ -0,0 +1,760 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerError,
CompilerErrorDetailOptions,
ErrorSeverity,
SourceLocation,
} from '..';
import {
ArrayExpression,
CallExpression,
Effect,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
Instruction,
InstructionId,
InstructionKind,
InstructionValue,
isUseEffectHookType,
LoadLocal,
makeInstructionId,
Place,
promoteTemporary,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {getOrInsertWith} from '../Utils/utils';
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
/*
* TODO(jmbrown):
* - traverse object methods
* - method calls
* - React.useEffect calls
*/
const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
export function transformFire(fn: HIRFunction): void {
const context = new Context(fn.env);
replaceFireFunctions(fn, context);
if (!context.hasErrors()) {
ensureNoMoreFireUses(fn, context);
}
context.throwIfErrorsFound();
}
function replaceFireFunctions(fn: HIRFunction, context: Context): void {
let hasRewrite = false;
for (const [, block] of fn.body.blocks) {
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
const deleteInstrs = new Set<InstructionId>();
for (const instr of block.instructions) {
const {value, lvalue} = instr;
if (
value.kind === 'CallExpression' &&
isUseEffectHookType(value.callee.identifier) &&
value.args.length > 0 &&
value.args[0].kind === 'Identifier'
) {
const lambda = context.getFunctionExpression(
value.args[0].identifier.id,
);
if (lambda != null) {
const capturedCallees =
visitFunctionExpressionAndPropagateFireDependencies(
lambda,
context,
true,
);
// Add useFire calls for all fire calls in found in the lambda
const newInstrs = [];
for (const [
fireCalleePlace,
fireCalleeInfo,
] of capturedCallees.entries()) {
if (!context.hasCalleeWithInsertedFire(fireCalleePlace)) {
context.addCalleeWithInsertedFire(fireCalleePlace);
const loadUseFireInstr = makeLoadUseFireInstruction(fn.env);
const loadFireCalleeInstr = makeLoadFireCalleeInstruction(
fn.env,
fireCalleeInfo.capturedCalleeIdentifier,
);
const callUseFireInstr = makeCallUseFireInstruction(
fn.env,
loadUseFireInstr.lvalue,
loadFireCalleeInstr.lvalue,
);
const storeUseFireInstr = makeStoreUseFireInstruction(
fn.env,
callUseFireInstr.lvalue,
fireCalleeInfo.fireFunctionBinding,
);
newInstrs.push(
loadUseFireInstr,
loadFireCalleeInstr,
callUseFireInstr,
storeUseFireInstr,
);
// We insert all of these instructions before the useEffect is loaded
const loadUseEffectInstrId = context.getLoadGlobalInstrId(
value.callee.identifier.id,
);
if (loadUseEffectInstrId == null) {
context.pushError({
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
reason: '[InsertFire] No LoadGlobal found for useEffect call',
suggestions: null,
});
continue;
}
rewriteInstrs.set(loadUseEffectInstrId, newInstrs);
}
}
ensureNoRemainingCalleeCaptures(
lambda.loweredFunc.func,
context,
capturedCallees,
);
if (
value.args.length > 1 &&
value.args[1] != null &&
value.args[1].kind === 'Identifier'
) {
const depArray = value.args[1];
const depArrayExpression = context.getArrayExpression(
depArray.identifier.id,
);
if (depArrayExpression != null) {
for (const dependency of depArrayExpression.elements) {
if (dependency.kind === 'Identifier') {
const loadOfDependency = context.getLoadLocalInstr(
dependency.identifier.id,
);
if (loadOfDependency != null) {
const replacedDepArrayItem = capturedCallees.get(
loadOfDependency.place.identifier.id,
);
if (replacedDepArrayItem != null) {
loadOfDependency.place =
replacedDepArrayItem.fireFunctionBinding;
}
}
}
}
} else {
context.pushError({
loc: value.args[1].loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else if (value.args.length > 1 && value.args[1].kind === 'Spread') {
context.pushError({
loc: value.args[1].place.loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
} else if (
value.kind === 'CallExpression' &&
value.callee.identifier.type.kind === 'Function' &&
value.callee.identifier.type.shapeId === BuiltInFireId &&
context.inUseEffectLambda()
) {
/*
* We found a fire(callExpr()) call. We remove the `fire()` call and replace the callExpr()
* with a freshly generated fire function binding. We'll insert the useFire call before the
* useEffect call, which happens in the CallExpression (useEffect) case above.
*/
/*
* We only allow fire to be called with a CallExpression: `fire(f())`
* TODO: add support for method calls: `fire(this.method())`
*/
if (value.args.length === 1 && value.args[0].kind === 'Identifier') {
const callExpr = context.getCallExpression(
value.args[0].identifier.id,
);
if (callExpr != null) {
const calleeId = callExpr.callee.identifier.id;
const loadLocal = context.getLoadLocalInstr(calleeId);
if (loadLocal == null) {
context.pushError({
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
reason:
'[InsertFire] No loadLocal found for fire call argument',
suggestions: null,
});
continue;
}
const fireFunctionBinding =
context.getOrGenerateFireFunctionBinding(
loadLocal.place,
value.loc,
);
loadLocal.place = {...fireFunctionBinding};
// Delete the fire call expression
deleteInstrs.add(instr.id);
} else {
context.pushError({
loc: value.loc,
description:
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else {
let description: string =
'fire() can only take in a single call expression as an argument';
if (value.args.length === 0) {
description += ' but received none';
} else if (value.args.length > 1) {
description += ' but received multiple arguments';
} else if (value.args[0].kind === 'Spread') {
description += ' but received a spread argument';
}
context.pushError({
loc: value.loc,
description,
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else if (value.kind === 'CallExpression') {
context.addCallExpression(lvalue.identifier.id, value);
} else if (
value.kind === 'FunctionExpression' &&
context.inUseEffectLambda()
) {
visitFunctionExpressionAndPropagateFireDependencies(
value,
context,
false,
);
} else if (value.kind === 'FunctionExpression') {
context.addFunctionExpression(lvalue.identifier.id, value);
} else if (value.kind === 'LoadLocal') {
context.addLoadLocalInstr(lvalue.identifier.id, value);
} else if (
value.kind === 'LoadGlobal' &&
value.binding.kind === 'ImportSpecifier' &&
value.binding.module === 'react' &&
value.binding.imported === 'fire' &&
context.inUseEffectLambda()
) {
deleteInstrs.add(instr.id);
} else if (value.kind === 'LoadGlobal') {
context.addLoadGlobalInstrId(lvalue.identifier.id, instr.id);
} else if (value.kind === 'ArrayExpression') {
context.addArrayExpression(lvalue.identifier.id, value);
}
}
block.instructions = rewriteInstructions(rewriteInstrs, block.instructions);
block.instructions = deleteInstructions(deleteInstrs, block.instructions);
if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) {
hasRewrite = true;
fn.env.hasFireRewrite = true;
}
}
if (hasRewrite) {
markInstructionIds(fn.body);
}
}
/**
* Traverses a function expression to find fire calls fire(foo()) and replaces them with
* fireFoo().
*
* When a function captures a fire call we need to update its context to reflect the newly created
* fire function bindings and update the LoadLocals referenced by the function's dependencies.
*
* @param isUseEffect is necessary so we can keep track of when we should additionally insert
* useFire hooks calls.
*/
function visitFunctionExpressionAndPropagateFireDependencies(
fnExpr: FunctionExpression,
context: Context,
enteringUseEffect: boolean,
): FireCalleesToFireFunctionBinding {
let withScope = enteringUseEffect
? context.withUseEffectLambdaScope.bind(context)
: context.withFunctionScope.bind(context);
const calleesCapturedByFnExpression = withScope(() =>
replaceFireFunctions(fnExpr.loweredFunc.func, context),
);
/*
* Make a mapping from each dependency to the corresponding LoadLocal for it so that
* we can replace the loaded place with the generated fire function binding
*/
const loadLocalsToDepLoads = new Map<IdentifierId, LoadLocal>();
for (const dep of fnExpr.loweredFunc.dependencies) {
const loadLocal = context.getLoadLocalInstr(dep.identifier.id);
if (loadLocal != null) {
loadLocalsToDepLoads.set(loadLocal.place.identifier.id, loadLocal);
}
}
const replacedCallees = new Map<IdentifierId, Place>();
for (const [
calleeIdentifierId,
loadedFireFunctionBindingPlace,
] of calleesCapturedByFnExpression.entries()) {
/*
* Given the ids of captured fire callees, look at the deps for loads of those identifiers
* and replace them with the new fire function binding
*/
const loadLocal = loadLocalsToDepLoads.get(calleeIdentifierId);
if (loadLocal == null) {
context.pushError({
loc: fnExpr.loc,
description: null,
severity: ErrorSeverity.Invariant,
reason:
'[InsertFire] No loadLocal found for fire call argument for lambda',
suggestions: null,
});
continue;
}
const oldPlaceId = loadLocal.place.identifier.id;
loadLocal.place = {
...loadedFireFunctionBindingPlace.fireFunctionBinding,
};
replacedCallees.set(
oldPlaceId,
loadedFireFunctionBindingPlace.fireFunctionBinding,
);
}
// For each replaced callee, update the context of the function expression to track it
for (
let contextIdx = 0;
contextIdx < fnExpr.loweredFunc.func.context.length;
contextIdx++
) {
const contextItem = fnExpr.loweredFunc.func.context[contextIdx];
const replacedCallee = replacedCallees.get(contextItem.identifier.id);
if (replacedCallee != null) {
fnExpr.loweredFunc.func.context[contextIdx] = replacedCallee;
}
}
context.mergeCalleesFromInnerScope(calleesCapturedByFnExpression);
return calleesCapturedByFnExpression;
}
/*
* eachInstructionOperand is not sufficient for our cases because:
* 1. fire is a global, which will not appear
* 2. The HIR may be malformed, so can't rely on function deps and must
* traverse the whole function.
*/
function* eachReachablePlace(fn: HIRFunction): Iterable<Place> {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
yield* eachReachablePlace(instr.value.loweredFunc.func);
} else {
yield* eachInstructionOperand(instr);
}
}
}
}
function ensureNoRemainingCalleeCaptures(
fn: HIRFunction,
context: Context,
capturedCallees: FireCalleesToFireFunctionBinding,
): void {
for (const place of eachReachablePlace(fn)) {
const calleeInfo = capturedCallees.get(place.identifier.id);
if (calleeInfo != null) {
const calleeName =
calleeInfo.capturedCalleeIdentifier.name?.kind === 'named'
? calleeInfo.capturedCalleeIdentifier.name.value
: '<unknown>';
context.pushError({
loc: place.loc,
description: `All uses of ${calleeName} must be either used with a fire() call in \
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
}
function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
for (const place of eachReachablePlace(fn)) {
if (
place.identifier.type.kind === 'Function' &&
place.identifier.type.shapeId === BuiltInFireId
) {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
}
function makeLoadUseFireInstruction(env: Environment): Instruction {
const useFirePlace = createTemporaryPlace(env, GeneratedSource);
useFirePlace.effect = Effect.Read;
useFirePlace.identifier.type = DefaultNonmutatingHook;
const instrValue: InstructionValue = {
kind: 'LoadGlobal',
binding: {
kind: 'ImportSpecifier',
name: 'useFire',
module: 'react',
imported: 'useFire',
},
loc: GeneratedSource,
};
return {
id: makeInstructionId(0),
value: instrValue,
lvalue: {...useFirePlace},
loc: GeneratedSource,
};
}
function makeLoadFireCalleeInstruction(
env: Environment,
fireCalleeIdentifier: Identifier,
): Instruction {
const loadedFireCallee = createTemporaryPlace(env, GeneratedSource);
const fireCallee: Place = {
kind: 'Identifier',
identifier: fireCalleeIdentifier,
reactive: false,
effect: Effect.Unknown,
loc: fireCalleeIdentifier.loc,
};
return {
id: makeInstructionId(0),
value: {
kind: 'LoadLocal',
loc: GeneratedSource,
place: {...fireCallee},
},
lvalue: {...loadedFireCallee},
loc: GeneratedSource,
};
}
function makeCallUseFireInstruction(
env: Environment,
useFirePlace: Place,
argPlace: Place,
): Instruction {
const useFireCallResultPlace = createTemporaryPlace(env, GeneratedSource);
useFireCallResultPlace.effect = Effect.Read;
const useFireCall: CallExpression = {
kind: 'CallExpression',
callee: {...useFirePlace},
args: [argPlace],
loc: GeneratedSource,
};
return {
id: makeInstructionId(0),
value: useFireCall,
lvalue: {...useFireCallResultPlace},
loc: GeneratedSource,
};
}
function makeStoreUseFireInstruction(
env: Environment,
useFireCallResultPlace: Place,
fireFunctionBindingPlace: Place,
): Instruction {
promoteTemporary(fireFunctionBindingPlace.identifier);
const fireFunctionBindingLValuePlace = createTemporaryPlace(
env,
GeneratedSource,
);
return {
id: makeInstructionId(0),
value: {
kind: 'StoreLocal',
lvalue: {
kind: InstructionKind.Const,
place: {...fireFunctionBindingPlace},
},
value: {...useFireCallResultPlace},
type: null,
loc: GeneratedSource,
},
lvalue: fireFunctionBindingLValuePlace,
loc: GeneratedSource,
};
}
type FireCalleesToFireFunctionBinding = Map<
IdentifierId,
{
fireFunctionBinding: Place;
capturedCalleeIdentifier: Identifier;
fireLoc: SourceLocation;
}
>;
class Context {
#env: Environment;
#errors: CompilerError = new CompilerError();
/*
* Used to look up the call expression passed to a `fire(callExpr())`. Gives back
* the `callExpr()`.
*/
#callExpressions = new Map<IdentifierId, CallExpression>();
/*
* We keep track of function expressions so that we can traverse them when
* we encounter a lambda passed to a useEffect call
*/
#functionExpressions = new Map<IdentifierId, FunctionExpression>();
/*
* Mapping from lvalue ids to the LoadLocal for it. Allows us to replace dependency LoadLocals.
*/
#loadLocals = new Map<IdentifierId, LoadLocal>();
/*
* Maps all of the fire callees found in a component/hook to the generated fire function places
* we create for them. Allows us to reuse already-inserted useFire results
*/
#fireCalleesToFireFunctions: Map<IdentifierId, Place> = new Map();
/*
* The callees for which we have already created fire bindings. Used to skip inserting a new
* useFire call for a fire callee if one has already been created.
*/
#calleesWithInsertedFire = new Set<IdentifierId>();
/*
* A mapping from fire callees to the created fire function bindings that are reachable from this
* scope.
*
* We additionally keep track of the captured callee identifier so that we can properly reference
* it in the place where we LoadLocal the callee as an argument to useFire.
*/
#capturedCalleeIdentifierIds: FireCalleesToFireFunctionBinding = new Map();
/*
* We only transform fire calls if we're syntactically within a useEffect lambda (for now)
*/
#inUseEffectLambda = false;
/*
* Mapping from useEffect callee identifier ids to the instruction id of the
* load global instruction for the useEffect call. We use this to insert the
* useFire calls before the useEffect call
*/
#loadGlobalInstructionIds = new Map<IdentifierId, InstructionId>();
constructor(env: Environment) {
this.#env = env;
}
/*
* We keep track of array expressions so we can rewrite dependency arrays passed to useEffect
* to use the fire functions
*/
#arrayExpressions = new Map<IdentifierId, ArrayExpression>();
pushError(error: CompilerErrorDetailOptions): void {
this.#errors.push(error);
}
withFunctionScope(fn: () => void): FireCalleesToFireFunctionBinding {
fn();
return this.#capturedCalleeIdentifierIds;
}
withUseEffectLambdaScope(fn: () => void): FireCalleesToFireFunctionBinding {
const capturedCalleeIdentifierIds = this.#capturedCalleeIdentifierIds;
const inUseEffectLambda = this.#inUseEffectLambda;
this.#capturedCalleeIdentifierIds = new Map();
this.#inUseEffectLambda = true;
const resultCapturedCalleeIdentifierIds = this.withFunctionScope(fn);
this.#capturedCalleeIdentifierIds = capturedCalleeIdentifierIds;
this.#inUseEffectLambda = inUseEffectLambda;
return resultCapturedCalleeIdentifierIds;
}
addCallExpression(id: IdentifierId, callExpr: CallExpression): void {
this.#callExpressions.set(id, callExpr);
}
getCallExpression(id: IdentifierId): CallExpression | undefined {
return this.#callExpressions.get(id);
}
addLoadLocalInstr(id: IdentifierId, loadLocal: LoadLocal): void {
this.#loadLocals.set(id, loadLocal);
}
getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined {
return this.#loadLocals.get(id);
}
getOrGenerateFireFunctionBinding(
callee: Place,
fireLoc: SourceLocation,
): Place {
const fireFunctionBinding = getOrInsertWith(
this.#fireCalleesToFireFunctions,
callee.identifier.id,
() => createTemporaryPlace(this.#env, GeneratedSource),
);
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
fireFunctionBinding,
capturedCalleeIdentifier: callee.identifier,
fireLoc,
});
return fireFunctionBinding;
}
mergeCalleesFromInnerScope(
innerCallees: FireCalleesToFireFunctionBinding,
): void {
for (const [id, calleeInfo] of innerCallees.entries()) {
this.#capturedCalleeIdentifierIds.set(id, calleeInfo);
}
}
addCalleeWithInsertedFire(id: IdentifierId): void {
this.#calleesWithInsertedFire.add(id);
}
hasCalleeWithInsertedFire(id: IdentifierId): boolean {
return this.#calleesWithInsertedFire.has(id);
}
inUseEffectLambda(): boolean {
return this.#inUseEffectLambda;
}
addFunctionExpression(id: IdentifierId, fn: FunctionExpression): void {
this.#functionExpressions.set(id, fn);
}
getFunctionExpression(id: IdentifierId): FunctionExpression | undefined {
return this.#functionExpressions.get(id);
}
addLoadGlobalInstrId(id: IdentifierId, instrId: InstructionId): void {
this.#loadGlobalInstructionIds.set(id, instrId);
}
getLoadGlobalInstrId(id: IdentifierId): InstructionId | undefined {
return this.#loadGlobalInstructionIds.get(id);
}
addArrayExpression(id: IdentifierId, array: ArrayExpression): void {
this.#arrayExpressions.set(id, array);
}
getArrayExpression(id: IdentifierId): ArrayExpression | undefined {
return this.#arrayExpressions.get(id);
}
hasErrors(): boolean {
return this.#errors.hasErrors();
}
throwIfErrorsFound(): void {
if (this.hasErrors()) throw this.#errors;
}
}
function deleteInstructions(
deleteInstrs: Set<InstructionId>,
instructions: Array<Instruction>,
): Array<Instruction> {
if (deleteInstrs.size > 0) {
const newInstrs = instructions.filter(instr => !deleteInstrs.has(instr.id));
return newInstrs;
}
return instructions;
}
function rewriteInstructions(
rewriteInstrs: Map<InstructionId, Array<Instruction>>,
instructions: Array<Instruction>,
): Array<Instruction> {
if (rewriteInstrs.size > 0) {
const newInstrs = [];
for (const instr of instructions) {
const newInstrsAtId = rewriteInstrs.get(instr.id);
if (newInstrsAtId != null) {
newInstrs.push(...newInstrsAtId, instr);
} else {
newInstrs.push(instr);
}
}
return newInstrs;
}
return instructions;
}

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export {transformFire} from './TransformFire';

View File

@@ -1,110 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import * as t from '@babel/types';
import chalk from 'chalk';
import {HIR, HIRFunction, ReactiveFunction} from '../HIR/HIR';
import {printFunctionWithOutlined, printHIR} from '../HIR/PrintHIR';
import {CodegenFunction} from '../ReactiveScopes';
import {printReactiveFunctionWithOutlined} from '../ReactiveScopes/PrintReactiveFunction';
let ENABLED: boolean = false;
let lastLogged: string;
export function toggleLogging(enabled: boolean): void {
ENABLED = enabled;
}
export function logDebug(step: string, value: string): void {
if (ENABLED) {
process.stdout.write(`${chalk.green(step)}:\n${value}\n\n`);
}
}
export function logHIR(step: string, ir: HIR): void {
if (ENABLED) {
const printed = printHIR(ir);
if (printed !== lastLogged) {
lastLogged = printed;
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
} else {
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
}
}
}
export function logCodegenFunction(step: string, fn: CodegenFunction): void {
if (ENABLED) {
let printed: string | null = null;
try {
const node = t.functionDeclaration(
fn.id,
fn.params,
fn.body,
fn.generator,
fn.async,
);
const ast = generate(node);
printed = ast.code;
} catch (e) {
let errMsg: string;
if (
typeof e === 'object' &&
e != null &&
'message' in e &&
typeof e.message === 'string'
) {
errMsg = e.message.toString();
} else {
errMsg = '[empty]';
}
console.log('Error formatting AST: ' + errMsg);
}
if (printed === null) {
return;
}
if (printed !== lastLogged) {
lastLogged = printed;
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
} else {
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
}
}
}
export function logHIRFunction(step: string, fn: HIRFunction): void {
if (ENABLED) {
const printed = printFunctionWithOutlined(fn);
if (printed !== lastLogged) {
lastLogged = printed;
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
} else {
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
}
}
}
export function logReactiveFunction(step: string, fn: ReactiveFunction): void {
if (ENABLED) {
const printed = printReactiveFunctionWithOutlined(fn);
if (printed !== lastLogged) {
lastLogged = printed;
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
} else {
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
}
}
}
export function log(fn: () => string): void {
if (ENABLED) {
const message = fn();
process.stdout.write(message.trim() + '\n\n');
}
}

View File

@@ -305,6 +305,14 @@ function validateNoRefAccessInRenderImpl(
);
break;
}
case 'TypeCastExpression': {
env.set(
instr.lvalue.identifier.id,
env.get(instr.value.value.identifier.id) ??
refTypeOfType(instr.lvalue),
);
break;
}
case 'LoadContext':
case 'LoadLocal': {
env.set(

View File

@@ -0,0 +1,60 @@
## Input
```javascript
import {useRef} from 'react';
function useArrayOfRef() {
const ref = useRef(null);
const callback = value => {
ref.current = value;
};
return [callback] as const;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {
useArrayOfRef();
return 'ok';
},
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
function useArrayOfRef() {
const $ = _c(1);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const callback = (value) => {
ref.current = value;
};
t0 = [callback];
$[0] = t0;
} else {
t0 = $[0];
}
return t0 as const;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {
useArrayOfRef();
return "ok";
},
params: [{}],
};
```
### Eval output
(kind: ok) "ok"

View File

@@ -0,0 +1,17 @@
import {useRef} from 'react';
function useArrayOfRef() {
const ref = useRef(null);
const callback = value => {
ref.current = value;
};
return [callback] as const;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {
useArrayOfRef();
return 'ok';
},
params: [{}],
};

View File

@@ -0,0 +1,129 @@
## Input
```javascript
import {makeArray, mutate} from 'shared-runtime';
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component({foo, bar}: {foo: number; bar: number}) {
let x = {foo};
let y: {bar: number; x?: {foo: number}} = {bar};
const f0 = function () {
let a = makeArray(y); // a = [y]
let b = x;
// this writes y.x = x
a[0].x = b;
};
f0();
mutate(y.x);
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 3, bar: 4}],
sequentialRenders: [
{foo: 3, bar: 4},
{foo: 3, bar: 5},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { makeArray, mutate } from "shared-runtime";
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component(t0) {
const $ = _c(5);
const { foo, bar } = t0;
let t1;
if ($[0] !== foo) {
t1 = { foo };
$[0] = foo;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
let y;
if ($[2] !== bar || $[3] !== x) {
y = { bar };
const f0 = function () {
const a = makeArray(y);
const b = x;
a[0].x = b;
};
f0();
mutate(y.x);
$[2] = bar;
$[3] = x;
$[4] = y;
} else {
y = $[4];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: 3, bar: 4 }],
sequentialRenders: [
{ foo: 3, bar: 4 },
{ foo: 3, bar: 5 },
],
};
```

View File

@@ -0,0 +1,48 @@
import {makeArray, mutate} from 'shared-runtime';
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component({foo, bar}: {foo: number; bar: number}) {
let x = {foo};
let y: {bar: number; x?: {foo: number}} = {bar};
const f0 = function () {
let a = makeArray(y); // a = [y]
let b = x;
// this writes y.x = x
a[0].x = b;
};
f0();
mutate(y.x);
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 3, bar: 4}],
sequentialRenders: [
{foo: 3, bar: 4},
{foo: 3, bar: 5},
],
};

View File

@@ -58,18 +58,16 @@ function Foo(t0) {
bar = $[1];
result = $[2];
}
const t1 = bar;
let t2;
if ($[3] !== result || $[4] !== t1) {
t2 = <Stringify result={result} fn={t1} shouldInvokeFns={true} />;
$[3] = result;
$[4] = t1;
$[5] = t2;
let t1;
if ($[3] !== bar || $[4] !== result) {
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
$[3] = bar;
$[4] = result;
$[5] = t1;
} else {
t2 = $[5];
t1 = $[5];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -43,16 +43,15 @@ function Component(props) {
} else {
x = $[1];
}
const t0 = x;
let t1;
if ($[2] !== t0) {
t1 = { x: t0 };
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== x) {
t0 = { x };
$[2] = x;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -42,16 +42,15 @@ function Component(props) {
} else {
x = $[1];
}
const t0 = x;
let t1;
if ($[2] !== t0) {
t1 = <div>{t0}</div>;
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== x) {
t0 = <div>{x}</div>;
$[2] = x;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -43,16 +43,15 @@ function Component(props) {
} else {
x = $[1];
}
const t0 = x;
let t1;
if ($[2] !== t0) {
t1 = { x: t0 };
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== x) {
t0 = { x };
$[2] = x;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -42,16 +42,15 @@ function Component(props) {
} else {
x = $[1];
}
const t0 = x;
let t1;
if ($[2] !== t0) {
t1 = { x: t0 };
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== x) {
t0 = { x };
$[2] = x;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -1,31 +0,0 @@
## Input
```javascript
function Component() {
const data = useData();
const items = [];
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<Stringify key={i} onClick={() => data.set(i)} />);
}
return items;
}
```
## Error
```
4 | // NOTE: `i` is a context variable because it's reassigned and also referenced
5 | // within a closure, the `onClick` handler of each item
> 6 | for (let i = MIN; i <= MAX; i += INCREMENT) {
| ^^^^^^^^^^^ Todo: Support for loops where the index variable is a context variable. `i` is a context variable (6:6)
7 | items.push(<Stringify key={i} onClick={() => data.set(i)} />);
8 | }
9 | return items;
```

View File

@@ -98,12 +98,6 @@ Todo: (BuildHIR::lowerExpression) Handle tagged template with interpolations (30
Todo: (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value (34:34)
Todo: (BuildHIR::lowerStatement) Handle Identifier inits in ForOfStatement (36:36)
Todo: (BuildHIR::lowerStatement) Handle ArrayPattern inits in ForOfStatement (38:38)
Todo: (BuildHIR::lowerStatement) Handle ObjectPattern inits in ForOfStatement (40:40)
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered (57:57)
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered (53:53)

View File

@@ -0,0 +1,78 @@
## Input
```javascript
function Component() {
const data = useData();
const items = [];
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<div key={i} onClick={() => data.set(i)} />);
}
return <>{items}</>;
}
const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
return new Map();
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component() {
const $ = _c(2);
const data = useData();
let t0;
if ($[0] !== data) {
const items = [];
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
items.push(<div key={i} onClick={() => data.set(i)} />);
}
t0 = <>{items}</>;
$[0] = data;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Map();
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
};
```
### Eval output
(kind: ok) <div></div><div></div><div></div><div></div>

View File

@@ -4,7 +4,20 @@ function Component() {
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<Stringify key={i} onClick={() => data.set(i)} />);
items.push(<div key={i} onClick={() => data.set(i)} />);
}
return items;
return <>{items}</>;
}
const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
return new Map();
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
};

View File

@@ -60,6 +60,10 @@ function ConditionalJsx({shouldWrap}) {
return content;
}
function ComponentWithSpreadPropsAndRef({ref, ...other}) {
return <Foo ref={ref} {...other} />;
}
// TODO: Support value blocks
function TernaryJsx({cond}) {
return cond ? <div /> : null;
@@ -409,6 +413,41 @@ function ConditionalJsx(t0) {
return content;
}
function ComponentWithSpreadPropsAndRef(t0) {
const $ = _c2(6);
let other;
let ref;
if ($[0] !== t0) {
({ ref, ...other } = t0);
$[0] = t0;
$[1] = other;
$[2] = ref;
} else {
other = $[1];
ref = $[2];
}
let t1;
if ($[3] !== other || $[4] !== ref) {
if (DEV) {
t1 = <Foo ref={ref} {...other} />;
} else {
t1 = {
$$typeof: Symbol.for("react.transitional.element"),
type: Foo,
ref: ref,
key: null,
props: { ref: ref, ...other },
};
}
$[3] = other;
$[4] = ref;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
// TODO: Support value blocks
function TernaryJsx(t0) {
const $ = _c2(2);

View File

@@ -56,6 +56,10 @@ function ConditionalJsx({shouldWrap}) {
return content;
}
function ComponentWithSpreadPropsAndRef({ref, ...other}) {
return <Foo ref={ref} {...other} />;
}
// TODO: Support value blocks
function TernaryJsx({cond}) {
return cond ? <div /> : null;

View File

@@ -3,7 +3,7 @@
```javascript
// @enableJsxOutlining
function Component(arr) {
function Component({arr}) {
const x = useX();
return arr.map(i => {
<>
@@ -49,12 +49,13 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableJsxOutlining
function Component(arr) {
function Component(t0) {
const $ = _c(3);
const { arr } = t0;
const x = useX();
let t0;
let t1;
if ($[0] !== arr || $[1] !== x) {
t0 = arr.map((i) => {
t1 = arr.map((i) => {
arr.map((i_0, id) => {
const T0 = _temp;
const child = <T0 i={i_0} x={x} />;
@@ -65,11 +66,11 @@ function Component(arr) {
});
$[0] = arr;
$[1] = x;
$[2] = t0;
$[2] = t1;
} else {
t0 = $[2];
t1 = $[2];
}
return t0;
return t1;
}
function _temp(t0) {
const $ = _c(5);
@@ -140,4 +141,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: exception) arr.map is not a function
(kind: ok) [null,null]

View File

@@ -1,5 +1,5 @@
// @enableJsxOutlining
function Component(arr) {
function Component({arr}) {
const x = useX();
return arr.map(i => {
<>

View File

@@ -33,17 +33,15 @@ function f(a) {
} else {
x = $[1];
}
const t0 = x;
let t1;
if ($[2] !== t0) {
t1 = <div x={t0} />;
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== x) {
t0 = <div x={x} />;
$[2] = x;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -1,53 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback} from 'react';
import {Stringify} from 'shared-runtime';
/**
* TODO: we're currently bailing out because `contextVar` is a context variable
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
* we took as eligible dependencies.
*
* One solution is to simply record `LoadContext` identifiers into the
* temporaries sidemap when the instruction occurs *after* the context
* variable's mutable range.
*/
function Foo(props) {
let contextVar;
if (props.cond) {
contextVar = {val: 2};
} else {
contextVar = {};
}
const cb = useCallback(() => [contextVar.val], [contextVar.val]);
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{cond: true}],
};
```
## Error
```
22 | }
23 |
> 24 | const cb = useCallback(() => [contextVar.val], [contextVar.val]);
| ^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (24:24)
25 |
26 | return <Stringify cb={cb} shouldInvokeFns={true} />;
27 | }
```

View File

@@ -0,0 +1,101 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback} from 'react';
import {Stringify} from 'shared-runtime';
/**
* TODO: we're currently bailing out because `contextVar` is a context variable
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
* we took as eligible dependencies.
*
* One solution is to simply record `LoadContext` identifiers into the
* temporaries sidemap when the instruction occurs *after* the context
* variable's mutable range.
*/
function Foo(props) {
let contextVar;
if (props.cond) {
contextVar = {val: 2};
} else {
contextVar = {};
}
const cb = useCallback(() => [contextVar.val], [contextVar.val]);
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{cond: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useCallback } from "react";
import { Stringify } from "shared-runtime";
/**
* TODO: we're currently bailing out because `contextVar` is a context variable
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
* we took as eligible dependencies.
*
* One solution is to simply record `LoadContext` identifiers into the
* temporaries sidemap when the instruction occurs *after* the context
* variable's mutable range.
*/
function Foo(props) {
const $ = _c(6);
let contextVar;
if ($[0] !== props.cond) {
if (props.cond) {
contextVar = { val: 2 };
} else {
contextVar = {};
}
$[0] = props.cond;
$[1] = contextVar;
} else {
contextVar = $[1];
}
let t0;
if ($[2] !== contextVar.val) {
t0 = () => [contextVar.val];
$[2] = contextVar.val;
$[3] = t0;
} else {
t0 = $[3];
}
contextVar;
const cb = t0;
let t1;
if ($[4] !== cb) {
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[4] = cb;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
};
```
### Eval output
(kind: ok) <div>{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}</div>

View File

@@ -44,16 +44,15 @@ function useFoo(arr1, arr2) {
y = $[2];
}
let t0;
const t1 = y;
let t2;
if ($[3] !== t1) {
t2 = { y: t1 };
$[3] = t1;
$[4] = t2;
let t1;
if ($[3] !== y) {
t1 = { y };
$[3] = y;
$[4] = t1;
} else {
t2 = $[4];
t1 = $[4];
}
t0 = t2;
t0 = t1;
return t0;
}

View File

@@ -36,17 +36,15 @@ function HomeDiscoStoreItemTileRating(props) {
} else {
count = $[1];
}
const t0 = count;
let t1;
if ($[2] !== t0) {
t1 = <Text>{t0}</Text>;
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== count) {
t0 = <Text>{count}</Text>;
$[2] = count;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
```

View File

@@ -0,0 +1,78 @@
## Input
```javascript
// @compilationMode(infer)
import {useMemo} from 'react';
import {ValidateMemoization} from 'shared-runtime';
function Component(props) {
const x = useMemo(() => props.x(), [props.x]);
return <ValidateMemoization inputs={[props.x]} output={x} />;
}
const f = () => ['React'];
const g = () => ['Compiler'];
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: () => ['React']}],
sequentialRenders: [{x: f}, {x: g}, {x: g}, {x: f}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
import { useMemo } from "react";
import { ValidateMemoization } from "shared-runtime";
function Component(props) {
const $ = _c(7);
let t0;
let t1;
if ($[0] !== props.x) {
t1 = props.x();
$[0] = props.x;
$[1] = t1;
} else {
t1 = $[1];
}
t0 = t1;
const x = t0;
let t2;
if ($[2] !== props.x) {
t2 = [props.x];
$[2] = props.x;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t2 || $[5] !== x) {
t3 = <ValidateMemoization inputs={t2} output={x} />;
$[4] = t2;
$[5] = x;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
const f = () => ["React"];
const g = () => ["Compiler"];
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: () => ["React"] }],
sequentialRenders: [{ x: f }, { x: g }, { x: g }, { x: f }],
};
```
### Eval output
(kind: ok) <div>{"inputs":["[[ function params=0 ]]"],"output":["React"]}</div>
<div>{"inputs":["[[ function params=0 ]]"],"output":["Compiler"]}</div>
<div>{"inputs":["[[ function params=0 ]]"],"output":["Compiler"]}</div>
<div>{"inputs":["[[ function params=0 ]]"],"output":["React"]}</div>

View File

@@ -0,0 +1,16 @@
// @compilationMode(infer)
import {useMemo} from 'react';
import {ValidateMemoization} from 'shared-runtime';
function Component(props) {
const x = useMemo(() => props.x(), [props.x]);
return <ValidateMemoization inputs={[props.x]} output={x} />;
}
const f = () => ['React'];
const g = () => ['Compiler'];
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: () => ['React']}],
sequentialRenders: [{x: f}, {x: g}, {x: g}, {x: f}],
};

View File

@@ -67,17 +67,15 @@ function Component(props) {
} else {
x = $[1];
}
const t0 = x;
let t1;
if ($[2] !== t0) {
t1 = [t0];
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== x) {
t0 = [x];
$[2] = x;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -0,0 +1,130 @@
## Input
```javascript
import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
/**
* Context variables are local variables that (1) have at least one reassignment
* and (2) are captured into a function expression. These have a known mutable
* range: from first declaration / assignment to the last direct or aliased,
* mutable reference.
*
* This fixture validates that forget can take granular dependencies on context
* variables when the reference to a context var happens *after* the end of its
* mutable range.
*/
function Component({cond, a}) {
let contextVar;
if (cond) {
contextVar = {val: a};
} else {
contextVar = {};
throwErrorWithMessage('');
}
const cb = {cb: () => contextVar.val * 4};
/**
* manually specify input to avoid adding a `PropertyLoad` from contextVar,
* which might affect hoistable-objects analysis.
*/
return (
<ValidateMemoization
inputs={[cond ? a : undefined]}
output={cb}
onlyCheckCompiled={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: false, a: undefined}],
sequentialRenders: [
{cond: true, a: 2},
{cond: true, a: 2},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { throwErrorWithMessage, ValidateMemoization } from "shared-runtime";
/**
* Context variables are local variables that (1) have at least one reassignment
* and (2) are captured into a function expression. These have a known mutable
* range: from first declaration / assignment to the last direct or aliased,
* mutable reference.
*
* This fixture validates that forget can take granular dependencies on context
* variables when the reference to a context var happens *after* the end of its
* mutable range.
*/
function Component(t0) {
const $ = _c(10);
const { cond, a } = t0;
let contextVar;
if ($[0] !== a || $[1] !== cond) {
if (cond) {
contextVar = { val: a };
} else {
contextVar = {};
throwErrorWithMessage("");
}
$[0] = a;
$[1] = cond;
$[2] = contextVar;
} else {
contextVar = $[2];
}
let t1;
if ($[3] !== contextVar.val) {
t1 = { cb: () => contextVar.val * 4 };
$[3] = contextVar.val;
$[4] = t1;
} else {
t1 = $[4];
}
const cb = t1;
const t2 = cond ? a : undefined;
let t3;
if ($[5] !== t2) {
t3 = [t2];
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] !== cb || $[8] !== t3) {
t4 = (
<ValidateMemoization inputs={t3} output={cb} onlyCheckCompiled={true} />
);
$[7] = cb;
$[8] = t3;
$[9] = t4;
} else {
t4 = $[9];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: false, a: undefined }],
sequentialRenders: [
{ cond: true, a: 2 },
{ cond: true, a: 2 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
<div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>

View File

@@ -0,0 +1,43 @@
import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
/**
* Context variables are local variables that (1) have at least one reassignment
* and (2) are captured into a function expression. These have a known mutable
* range: from first declaration / assignment to the last direct or aliased,
* mutable reference.
*
* This fixture validates that forget can take granular dependencies on context
* variables when the reference to a context var happens *after* the end of its
* mutable range.
*/
function Component({cond, a}) {
let contextVar;
if (cond) {
contextVar = {val: a};
} else {
contextVar = {};
throwErrorWithMessage('');
}
const cb = {cb: () => contextVar.val * 4};
/**
* manually specify input to avoid adding a `PropertyLoad` from contextVar,
* which might affect hoistable-objects analysis.
*/
return (
<ValidateMemoization
inputs={[cond ? a : undefined]}
output={cb}
onlyCheckCompiled={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: false, a: undefined}],
sequentialRenders: [
{cond: true, a: 2},
{cond: true, a: 2},
],
};

View File

@@ -35,17 +35,15 @@ function HomeDiscoStoreItemTileRating(props) {
} else {
count = $[1];
}
const t0 = count;
let t1;
if ($[2] !== t0) {
t1 = <Text>{t0}</Text>;
$[2] = t0;
$[3] = t1;
let t0;
if ($[2] !== count) {
t0 = <Text>{count}</Text>;
$[2] = count;
$[3] = t0;
} else {
t1 = $[3];
t0 = $[3];
}
return t1;
return t0;
}
```

View File

@@ -0,0 +1,49 @@
## Input
```javascript
import {arrayPush} from 'shared-runtime';
function useFoo({a, b}) {
const obj = {a};
arrayPush(Object.keys(obj), b);
return obj;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 3}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const { a, b } = t0;
let t1;
if ($[0] !== a) {
t1 = { a };
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const obj = t1;
arrayPush(Object.keys(obj), b);
return obj;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 3 }],
};
```
### Eval output
(kind: ok) {"a":2}

View File

@@ -0,0 +1,11 @@
import {arrayPush} from 'shared-runtime';
function useFoo({a, b}) {
const obj = {a};
arrayPush(Object.keys(obj), b);
return obj;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 3}],
};

View File

@@ -0,0 +1,53 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
fire(foo(props));
});
return null;
}
```
## Code
```javascript
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";
function Component(props) {
const $ = _c(3);
const foo = _temp;
const t0 = useFire(foo);
let t1;
if ($[0] !== props || $[1] !== t0) {
t1 = () => {
t0(props);
};
$[0] = props;
$[1] = t0;
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t1);
return null;
}
function _temp(props_0) {
console.log(props_0);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,13 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
fire(foo(props));
});
return null;
}

View File

@@ -0,0 +1,74 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
function nested() {
function nestedAgain() {
function nestedThrice() {
fire(foo(props));
}
nestedThrice();
}
nestedAgain();
}
nested();
});
return null;
}
```
## Code
```javascript
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";
function Component(props) {
const $ = _c(3);
const foo = _temp;
const t0 = useFire(foo);
let t1;
if ($[0] !== props || $[1] !== t0) {
t1 = () => {
const nested = function nested() {
const nestedAgain = function nestedAgain() {
const nestedThrice = function nestedThrice() {
t0(props);
};
nestedThrice();
};
nestedAgain();
};
nested();
};
$[0] = props;
$[1] = t0;
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t1);
return null;
}
function _temp(props_0) {
console.log(props_0);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,22 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
function nested() {
function nestedAgain() {
function nestedThrice() {
fire(foo(props));
}
nestedThrice();
}
nestedAgain();
}
nested();
});
return null;
}

View File

@@ -0,0 +1,37 @@
## Input
```javascript
// @enableFire
import {fire, useEffect} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
if (props.cond) {
useEffect(() => {
fire(foo(props));
});
}
return null;
}
```
## Error
```
8 |
9 | if (props.cond) {
> 10 | useEffect(() => {
| ^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (10:10)
11 | fire(foo(props));
12 | });
13 | }
```

View File

@@ -0,0 +1,16 @@
// @enableFire
import {fire, useEffect} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
if (props.cond) {
useEffect(() => {
fire(foo(props));
});
}
return null;
}

View File

@@ -0,0 +1,39 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
function nested() {
fire(foo(props));
foo(props);
}
nested();
});
return null;
}
```
## Error
```
9 | function nested() {
10 | fire(foo(props));
> 11 | foo(props);
| ^^^ InvalidReact: Cannot compile `fire`. All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect (11:11)
12 | }
13 |
14 | nested();
```

View File

@@ -0,0 +1,18 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
function nested() {
fire(foo(props));
foo(props);
}
nested();
});
return null;
}

View File

@@ -0,0 +1,34 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component({bar, baz}) {
const foo = () => {
console.log(bar, baz);
};
useEffect(() => {
fire(foo(bar), baz);
});
return null;
}
```
## Error
```
7 | };
8 | useEffect(() => {
> 9 | fire(foo(bar), baz);
| ^^^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received multiple arguments (9:9)
10 | });
11 |
12 | return null;
```

View File

@@ -0,0 +1,13 @@
// @enableFire
import {fire} from 'react';
function Component({bar, baz}) {
const foo = () => {
console.log(bar, baz);
};
useEffect(() => {
fire(foo(bar), baz);
});
return null;
}

View File

@@ -0,0 +1,40 @@
## Input
```javascript
// @enable
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
useEffect(() => {
function nested() {
fire(foo(props));
}
nested();
});
});
return null;
}
```
## Error
```
7 | };
8 | useEffect(() => {
> 9 | useEffect(() => {
| ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (9:9)
10 | function nested() {
11 | fire(foo(props));
12 | }
```

View File

@@ -0,0 +1,19 @@
// @enable
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
useEffect(() => {
function nested() {
fire(foo(props));
}
nested();
});
});
return null;
}

View File

@@ -0,0 +1,34 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = () => {
console.log(props);
};
useEffect(() => {
fire(props);
});
return null;
}
```
## Error
```
7 | };
8 | useEffect(() => {
> 9 | fire(props);
| ^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9)
10 | });
11 |
12 | return null;
```

View File

@@ -0,0 +1,13 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = () => {
console.log(props);
};
useEffect(() => {
fire(props);
});
return null;
}

View File

@@ -0,0 +1,38 @@
## Input
```javascript
// @enableFire
import {fire, useCallback} from 'react';
function Component({props, bar}) {
const foo = () => {
console.log(props);
};
fire(foo(props));
useCallback(() => {
fire(foo(props));
}, [foo, props]);
return null;
}
```
## Error
```
6 | console.log(props);
7 | };
> 8 | fire(foo(props));
| ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8)
Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11)
9 |
10 | useCallback(() => {
11 | fire(foo(props));
```

View File

@@ -0,0 +1,15 @@
// @enableFire
import {fire, useCallback} from 'react';
function Component({props, bar}) {
const foo = () => {
console.log(props);
};
fire(foo(props));
useCallback(() => {
fire(foo(props));
}, [foo, props]);
return null;
}

View File

@@ -0,0 +1,37 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
const deps = [foo, props];
useEffect(() => {
fire(foo(props));
}, deps);
return null;
}
```
## Error
```
11 | useEffect(() => {
12 | fire(foo(props));
> 13 | }, deps);
| ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13)
14 |
15 | return null;
16 | }
```

View File

@@ -0,0 +1,16 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
const deps = [foo, props];
useEffect(() => {
fire(foo(props));
}, deps);
return null;
}

View File

@@ -0,0 +1,40 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
const deps = [foo, props];
useEffect(
() => {
fire(foo(props));
},
...deps
);
return null;
}
```
## Error
```
13 | fire(foo(props));
14 | },
> 15 | ...deps
| ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (15:15)
16 | );
17 |
18 | return null;
```

View File

@@ -0,0 +1,19 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = props => {
console.log(props);
};
const deps = [foo, props];
useEffect(
() => {
fire(foo(props));
},
...deps
);
return null;
}

View File

@@ -0,0 +1,34 @@
## Input
```javascript
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = () => {
console.log(props);
};
useEffect(() => {
fire(...foo);
});
return null;
}
```
## Error
```
7 | };
8 | useEffect(() => {
> 9 | fire(...foo);
| ^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received a spread argument (9:9)
10 | });
11 |
12 | return null;
```

View File

@@ -0,0 +1,13 @@
// @enableFire
import {fire} from 'react';
function Component(props) {
const foo = () => {
console.log(props);
};
useEffect(() => {
fire(...foo);
});
return null;
}

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