Compare commits

...

59 Commits

Author SHA1 Message Date
Joe Savona
eaf36e3188 Update on "[compiler] Null out source locations where not explicitly preserved"
Inspired by #32950. Specifically from https://github.com/facebook/react/issues/32950#issuecomment-2837887871, it sounds like Babel by default emits source map information for all nodes, even when they don't have a `loc` property set. Code coverage tools then pick up the synthesized source location information for this, leading to the issue described.

A few google searches didn't turn up any documented way to opt-out of generating source span information, but AI tools answer that explicitly setting `node.loc = null` omits the node from source maps. This PR is a quick hack to confirm that.

[ghstack-poisoned]
2025-04-30 09:58:11 +09:00
Joe Savona
9db3f8b484 Update on "[compiler] Null out source locations where not explicitly preserved"
Inspired by #32950. Specifically from https://github.com/facebook/react/issues/32950#issuecomment-2837887871, it sounds like Babel by default emits source map information for all nodes, even when they don't have a `loc` property set. Code coverage tools then pick up the synthesized source location information for this, leading to the issue described.

A few google searches didn't turn up any documented way to opt-out of generating source span information, but AI tools answer that explicitly setting `node.loc = null` omits the node from source maps. This PR is a quick hack to confirm that.

[ghstack-poisoned]
2025-04-30 09:54:19 +09:00
Joe Savona
06beb9f43e [compiler] Null out source locations where not explicitly preserved
Inspired by #32950. Specifically from https://github.com/facebook/react/issues/32950#issuecomment-2837887871, it sounds like Babel by default emits source map information for all nodes, even when they don't have a `loc` property set. Code coverage tools then pick up the synthesized source location information for this, leading to the issue described.

A few google searches didn't turn up any documented way to opt-out of generating source span information, but AI tools answer that explicitly setting `node.loc = null` omits the node from source maps. This PR is a quick hack to confirm that.

[ghstack-poisoned]
2025-04-29 18:06:45 +09:00
Sebastian Markbåge
5dc00d6b2b [Fizz] Reset Instructions on ResumableState (#33046)
When we end up creating an incomplete state in the shell we end up not
flushing anything. As a hack, in this case we need to reset the
ResumableState because some of the ResumableState is still relevant
(e.g. any preloads that went into headers) but some of the
ResumableState needs to be reset since they assume that what we produced
actually flushed.

We didn't reset the instructions state but we haven't actually flushed
any of the instructions so it needs to reset.
2025-04-28 15:50:06 -04:00
Sebastian "Sebbie" Silbermann
c498bfce8b [devtools] Allow inspecting cause, name, message, stack of Errors in props (#33023) 2025-04-26 07:20:57 +02:00
Sebastian Markbåge
8e9a5fc6c1 [Fizz] Enable the progressiveChunkSize option (#33027)
Since the very beginning we have had the `progressiveChunkSize` option
but we never actually took advantage of it because we didn't count the
bytes that we emitted. This starts counting the bytes by taking a pass
over the added chunks each time a segment completes.

That allows us to outline a Suspense boundary to stream in late even if
it is already loaded by the time that back-pressure flow and in a
`prerender`. Meaning it gets inserted with script.

The effect can be seen in the fixture where if you have large HTML
content that can block initial paint (thanks to
[`rel="expect"`](https://github.com/facebook/react/pull/33016) but also
nested Suspense boundaries). Before this fix, the paint would be blocked
until the large content loaded. This lets us paint the fallback first in
the case that the raw bytes of the content takes a while to download.

You can set it to `Infinity` to opt-out. E.g. if you want to ensure
there's never any scripts. It's always set to `Infinity` in
`renderToHTML` and the legacy `renderToString`.

One downside is that if we might choose to outline a boundary, we need
to let its fallback complete.

We don't currently discount the size of the fallback but really just
consider them additive even though in theory the fallback itself could
also add significant size or even more than the content. It should maybe
really be considered the delta but that would require us to track the
size of the fallback separately which is tricky.

One problem with the current heuristic is that we just consider the size
of the boundary content itself down to the next boundary. If you have a
lot of small boundaries adding up, it'll never kick in. I intend to
address that in a follow up.
2025-04-25 16:10:53 -04:00
mofeiZ
89e8875ec4 [compiler] Fallback for inferred effect dependencies (#32984)
When effect dependencies cannot be inferred due to memoization-related
bailouts or unexpected mutable ranges (which currently often have to do
with writes to refs), fall back to traversing the effect lambda itself.

This fallback uses the same logic as PropagateScopeDependencies:
1. Collect a sidemap of loads and property loads
2. Find hoistable accesses from the control flow graph. Note that here,
we currently take into account the mutable ranges of instructions (see
`mutate-after-useeffect-granular-access` fixture)
3. Collect the set of property paths accessed by the effect
4. Merge to get the set of minimal dependencies
2025-04-25 15:44:39 -04:00
mofeiZ
2d0a5e399f [compiler] Patch for reactive refs in inferred effect dependencies (#32991)
Inferred effect dependencies and inlined jsx (both experimental
features) rely on `InferReactivePlaces` to determine their dependencies.


Since adding type inference for phi nodes
(https://github.com/facebook/react/pull/30796), we have been incorrectly
inferring stable-typed value blocks (e.g. `props.cond ? setState1 :
setState2`) as non-reactive. This fix patches InferReactivePlaces
instead of adding a new pass since we want non-reactivity propagated
correctly
2025-04-25 15:42:40 -04:00
mofeiZ
0c28a09eef [ci] Reduce non-deterministic builds for eslint-plugin-react-hooks (#33026)
See https://github.com/rollup/plugins/issues/1425

Currently, `@babel/helper-string-parser/lib/index.js` is either emitted
as a wrapped esmodule or inline depending on the ordering of async
functions in `rollup/commonjs`. Specifically,
`@babel/types/lib/definitions/core.js` is cyclic (i.e. transitively
depends upon itself), but sometimes
`@babel/helper-string-parser/lib/index.js` is emitted before this is
realized.


A relatively straightforward patch is to wrap all modules (see
https://github.com/rollup/plugins/issues/1425#issuecomment-1465626736).
This only regresses `eslint-plugin-react-hooks` bundle size by ~1.8% and
is safer (see
https://github.com/rollup/plugins/blob/master/packages/commonjs/README.md#strictrequires)

> The default value of true will wrap all CommonJS files in functions
which are executed when they are required for the first time, preserving
NodeJS semantics. This is the safest setting and should be used if the
generated code does not work correctly with "auto". Note that
strictRequires: true can have a small impact on the size and performance
of generated code, but less so if the code is minified.

(note that we're on an earlier version of `@rollup/commonjs` which does
not default to `strictRequires: true`)
2025-04-25 14:26:59 -04:00
Sebastian Markbåge
143d3e1b89 [Fizz] Emit link rel="expect" to block render before the shell has fully loaded (#33016)
The semantics of React is that anything outside of Suspense boundaries
in a transition doesn't display until it has fully unsuspended. With SSR
streaming the intention is to preserve that.

We explicitly don't want to support the mode of document streaming
normally supported by the browser where it can paint content as tags
stream in since that leads to content popping in and thrashing in
unpredictable ways. This should instead be modeled explictly by nested
Suspense boundaries or something like SuspenseList.

After the first shell any nested Suspense boundaries are only revealed,
by script, once they're fully streamed in to the next boundary. So this
is already the case there. However, for the initial shell we have been
at the mercy of browser heuristics for how long it decides to stream
before the first paint.

Chromium now has [an API explicitly for this use
case](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#stabilizing_page_state_to_make_cross-document_transitions_consistent)
that lets us model the semantics that we want. This is always important
but especially so with MPA View Transitions.

After this a simple document looks like this:

```html
<!DOCTYPE html>
<html>
  <head>
     <link rel="expect" href="#«R»" blocking="render"/>
  </head>
  <body>
    <p>hello world</p>
    <script src="bootstrap.js" id="«R»" async=""></script>
    ...
  </body>
</html>
```

The `rel="expect"` tag indicates that we want to wait to paint until we
have streamed far enough to be able to paint the id `"«R»"` which
indicates the shell.

Ideally this `id` would be assigned to the root most HTML element in the
body. However, this is tricky in our implementation because there can be
multiple and we can render them out of order.

So instead, we assign the id to the first bootstrap script if there is
one since these are always added to the end of the shell. If there isn't
a bootstrap script then we emit an empty `<template
id="«R»"></template>` instead as a marker.

Since we currently put as much as possible in the shell if it's loaded
by the time we render, this can have some negative effects for very
large documents. We should instead apply the heuristic where very large
Suspense boundaries get outlined outside the shell even if they're
immediately available. This means that even prerenders can end up with
script tags.

We only emit the `rel="expect"` if you're rendering a whole document.
I.e. if you rendered either a `<html>` or `<head>` tag. If you're
rendering a partial document, then we don't really know where the
streaming parts are anyway and can't provide such guarantees. This does
apply whether you're streaming or not because we still want to block
rendering until the end, but in practice any serialized state that needs
hydrate should still be embedded after the completion id.
2025-04-25 11:52:28 -04:00
Sebastian Markbåge
693803a9bb Rename Suspense unstable_name to name (#33014)
This was only used by Transition Tracing which isn't really used
anywhere.

However, we want to start using it for other DevTools.
2025-04-24 16:53:34 -04:00
lauren
24dfad3abb [compiler] Add changelog (#32983)
Adds CHANGELOG.md.

This entry contains changes from the very first beta
`19.0.0-beta-9ee70a1-20241017` to `19.1.0-rc.1`.
2025-04-24 16:20:02 -04:00
lauren
bb74190c26 [mcp] Convert docs resource to tool (#33009)
Seems to work better as a tool. Also it now returns plaintext instead of
markdown.
2025-04-24 14:57:44 -04:00
lauren
5010364d34 [chore] Update caniuse-lite (#33013)
silence annoying warnings

```
npx update-browserslist-db@latest
```
2025-04-24 13:50:03 -04:00
lauren
9938f83ca2 [compiler] Emit CompileSkip before CompileSuccess event (#33012)
Previously the CompileSuccess event would emit first before CompileSkip,
so the lsp's codelens would incorrectly flag skipped components/hooks
(via 'use no memo') as being optimized.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33012).
* __->__ #33012
* #33011
* #33010
2025-04-24 13:30:36 -04:00
lauren
2af218a728 [forgive][ez] Tweak logging (#33011)
Just some tweaks

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33011).
* #33012
* __->__ #33011
* #33010
2025-04-24 13:30:16 -04:00
lauren
b06bb35ce9 [forgive] Don't look up user babel configs (#33010)
Projects with existing babel config files may confuse the LSP, so
explictly opt out of looking them up.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33010).
* #33012
* #33011
* __->__ #33010
2025-04-24 13:29:56 -04:00
Sebastian "Sebbie" Silbermann
197d6a0403 [devtools] 1st class support of used Thenables (#32989)
Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>
2025-04-24 13:46:31 +02:00
lauren
ad09027c16 [compiler] Add missing copyrights (#33004)
`yarn copyright`
2025-04-23 22:04:44 -04:00
lauren
8b9629c810 [compiler] Fix copyright script (#33003)
Don't try to open directories
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33003).
* #33004
* __->__ #33003
* #33002

---------

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 21:55:24 -04:00
lauren
3a5335676f [forgive] Polish decorations (#33002)
Polishes up decorations.

Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33002).
* #33004
* #33003
* __->__ #33002

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 21:55:15 -04:00
lauren
b75af04670 [forgive] Don't crash if we couldn't compile (#33001)
Compiler shouldn't crash Forgive if it can't compile (eg parse error due
to being mid-typing).

Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33001).
* #33002
* __->__ #33001
* #33000

---------

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 21:32:11 -04:00
lauren
f765082996 [forgive] Add code action to remove dependency array (#33000)
Adds a new codeaction event in the compiler and handler in forgive. This
allows you to remove a dependency array when you're editing a range that
is within an autodep eligible function.

Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33000).
* #33002
* #33001
* __->__ #33000

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 21:31:57 -04:00
lauren
7b21c46489 [forgive] Refactor inferred deps (#32999)
Refactor.

Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32999).
* #33002
* #33001
* #33000
* __->__ #32999

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 21:27:22 -04:00
lauren
e25e8c7575 [forgive] Hacky first pass at adding decorations for inferred deps (#32998)
Draws basic decorations for inferred deps on hover.

Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32998).
* #33002
* #33001
* #33000
* #32999
* __->__ #32998

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 21:21:44 -04:00
lauren
cd7d236682 [forgive] Emit AutoDepsDecoration event when inferring effect deps (#32997)
Emits a new event for decorating inferred effect dependencies.

Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32997).
* #33002
* #33001
* #33000
* #32999
* #32998
* __->__ #32997
* #32996

---------

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 20:51:38 -04:00
lauren
71d0896a4a [forgive] Log inferEffectDependencies (#32996)
This was missed earlier.


Co-authored-by: Jordan Brown <jmbrown@meta.com>
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32996).
* #33002
* #33001
* #33000
* #32999
* #32998
* #32997
* __->__ #32996

Co-authored-by: Jordan Brown <jmbrown@meta.com>
2025-04-23 20:49:25 -04:00
Hendrik Liebau
914319ae59 [Flight] Don't hang forever when prerendering a rejected promise (#32953) 2025-04-23 11:02:43 +02:00
Sebastian Markbåge
3ef31d196a Implement Partial Hydration for Activity (#32863)
Stacked on #32862 and #32842.

This means that Activity boundaries now act as boundaries which can have
their effects mounted independently. Just like Suspense boundaries, we
hydrate the outer content first and then start hydrating the content in
an Offscreen lane. Flowing props or interacting with the content
increases the priority just like Suspense boundaries.

This skips emitting even the comments for `<Activity mode="hidden">` so
we don't hydrate those. Instead those are deferred to a later client
render.

The implementation are just forked copies of the SuspenseComponent
branches and then carefully going through each line and tweaking it.

The main interesting bit is that, unlike Suspense, Activity boundaries
don't have fallbacks so all those branches where you might commit a
suspended tree disappears. Instead, if something suspends while
hydration, we can just leave the dehydrated content in place. However,
if something does suspend during client rendering then it should bubble
up to the parent. Therefore, we have to be careful to only
pushSuspenseHandler when hydrating. That's really the main difference.

This just uses the existing basic Activity tests but I've started work
on port all of the applicable Suspense tests in SelectiveHydration-test
and PartialHydration-test to Activity versions.
2025-04-22 21:00:30 -04:00
Sebastian Markbåge
17f88c80ed Implement ActivityInstance in FiberConfigDOM (#32842)
Stacked on #32851 and #32900.

This implements the equivalent Configs for ActivityInstance as we have
for SuspenseInstance. These can be implemented as comments but they
don't have to be and can be implemented differently in the renderer.

This seems like a lot duplication but it's actually ends mostly just
calling the same methods underneath and the wrappers compiles out.

This doesn't leave the Activity dehydrated yet. It just hydrates into it
immediately.
2025-04-22 19:44:14 -04:00
Sebastian Markbåge
3fbd6b7b50 Set hidden Offscreen to the shellBoundary regardless of previous state (#32844)
I think this was probably just copy-paste from the Suspense path.

It shouldn't matter what the previous state of an Offscreen boundary
was. What matters is that it's now hidden and therefore if it suspends,
we can just leave it as is without the tree becoming inconsistent.
2025-04-22 19:39:09 -04:00
Sebastian Markbåge
ebf7318e87 Hide/unhide the content of dehydrated suspense boundaries if they resuspend (#32900)
Found this bug while working on Activity. There's a weird edge case when
a dehydrated Suspense boundary is a direct child of another Suspense
boundary which is hydrated but then it resuspends without forcing the
inner one to hydrate/delete.

It used to just leave that in place because hiding/unhiding didn't deal
with dehydrated fragments.

Not sure this is really worth fixing.
2025-04-22 19:29:12 -04:00
Hendrik Liebau
620c838fb6 Build react-server-dom-webpack for codesandbox (#32990)
This allows us to test Flight changes in a codesandbox.

[Example](https://codesandbox.io/p/devbox/zkjk7y)
2025-04-22 22:20:21 +02:00
lauren
7213509649 [compiler] Only append hash and date for experimental releases (#32981)
No need to append these for non experimental/beta releases.
2025-04-21 15:10:51 -04:00
lauren
4c54da77fb [ci] Change to string type (#32980)
to no one's surprise, the `number` type appears to be cursed in GH
actions for workflow dispatch. switch to string
2025-04-21 14:56:51 -04:00
lauren
efd890422d [compiler] Fix version name in publish script (#32979)
Add ability to specify an optional tagVersion which is appended to the
version name + tag, eg

19.1.0-rc.1
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32979).
* __->__ #32979
* #32978
2025-04-21 14:43:20 -04:00
lauren
b303610c33 [eprh] Bump stable version (#32978)
https://www.npmjs.com/package/eslint-plugin-react-hooks/v/6.0.0 was just
released, so we can bump this now.
2025-04-21 14:36:13 -04:00
lauren
fea92d8462 [ci] Remove compiler weekly release (#32977)
No longer needed.
2025-04-21 13:47:50 -04:00
Sebastian "Sebbie" Silbermann
bc6184dd99 [devtools] Fix "View source" for sources with URLs that aren't normalized (#32951) 2025-04-17 21:56:05 +02:00
lauren
ce578f9c59 [compiler] Update publish tags (#32952)
Adds missing tag.
2025-04-17 13:13:50 -04:00
lauren
45d942f94a [mcp] Also emit bailout messages with no loc (#32937)
Not every bailout will contain a loc (could be synthetic)
2025-04-17 13:11:55 -04:00
Jordan Brown
b8bedc267f [compiler][autodeps/fire] Do not include fire functions in autodep arrays (#32532)
Summary: We landed on not including fire functions in dep arrays. They
aren't needed because all values returned from the useFire hook call
will read from the same ref. The linter will error if you include a
fired function in an explicit dep array.

Test Plan: yarn snap --watch

--
2025-04-17 13:03:19 -04:00
lauren
4a36d3eab7 [ci] Only label on PR open (#32936)
No reason to label it every update, only do it once when it's first
opened.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32936).
* __->__ #32936
* #32935
2025-04-16 18:01:55 -04:00
lauren
2ddf8caa9d [ci] Fix check_access again (#32935)
I can see the value being output and set correctly but not sure why it's
skipping the 2nd job.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32935).
* #32936
* __->__ #32935
2025-04-16 18:00:25 -04:00
lauren
95ff37f5f5 [mcp] Iterate on prompt (#32932)
v2
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32932).
* __->__ #32932
* #32931
* #32930
* #32929
* #32928
2025-04-16 17:49:25 -04:00
lauren
3c75bf21dd [mcp] Fix bailout loc (#32931)
Use the correct loc line numbers and not [Object:object]
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32931).
* #32932
* __->__ #32931
* #32930
* #32929
* #32928
2025-04-16 17:49:15 -04:00
lauren
3e04b2a214 [mcp] Refine passes returned (#32930)
Adds some new options to request the HIR, ReactiveFunction passes
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32930).
* #32932
* #32931
* __->__ #32930
* #32929
* #32928
2025-04-16 17:49:04 -04:00
lauren
fc21d5a7db [mcp] Dedupe docs (#32929)
Previously the resource would return a bunch of dupes because the
algolia results would return multiple hashes (headings) for the same
url.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32929).
* #32932
* #32931
* #32930
* __->__ #32929
* #32928
2025-04-16 17:48:53 -04:00
lauren
35ab8ffef7 [mcp] Add inspect script (#32928)
Uses https://github.com/modelcontextprotocol/inspector to inspect and
debug the mcp server.

`yarn workspace react-mcp-server dev` will build the server in watch
mode and launch the inspector. Default address is http://127.0.0.1:6274.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32928).
* #32932
* #32931
* #32930
* #32929
* __->__ #32928
2025-04-16 17:48:38 -04:00
lauren
68013725ac [ci] Fix check_access fr (#32934)
💀
2025-04-16 17:39:24 -04:00
lauren
bf39780a06 [ci] Fix check_access output (#32933)
the joy of yml
2025-04-16 17:27:09 -04:00
Sebastian Markbåge
b04254fdce Don't try to hydrate a hidden Offscreen tree (#32862)
I found a bug even before the Activity hydration stuff.

If we're hydrating an Offscreen boundary in its "hidden" state it won't
have any content to hydrate so will trigger hydration errors (which are
then eaten by the Offscreen boundary itself). Leaving it not prewarmed.

This doesn't happen in the simple case because we'd be hydrating at a
higher priority than Offscreen at the root, and those are deferred to
Offscreen by not having higher priority. However, we've hydrating at the
Offscreen priority, which we do inside Suspense boundaries, then it
tries to hydrate against an empty set.

I ended up moving this to the Activity boundary in a future PR since
it's the SSR side that decided where to not render something and it only
has a concept of Activity, no Offscreen.


1dc05a5e22 (diff-d5166797ebbc5b646a49e6a06a049330ca617985d7a6edf3ad1641b43fde1ddfR1111)
2025-04-15 17:43:42 -04:00
Sebastian Markbåge
539bbdbd86 Warn if you pass a hidden prop to Activity (#32916)
Since `hidden` is a prop on arbitrary DOM elements it's a common mistake
to think that it would also work that way on `<Activity>` but it
doesn't. In fact, we even had this mistakes in our own tests.

Maybe there's an argument that we should actually just support it but we
also have more modes planned.

So this adds a warning. It should also already be covered by TypeScript.
2025-04-15 17:17:22 -04:00
lauren
e71d4205ae [ci] Don't run some checks for non-members/collaborators (#32918)
There's really no need to even run the workflow for non-members or
collaborators for the labeling and discord notification workflows. We
can exit early.
2025-04-15 13:02:16 -04:00
lauren
2ed34eba0d Update @playwright/test (#32917)
Routine update.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32917).
* #32918
* __->__ #32917
2025-04-15 12:52:43 -04:00
Jorge (Hezi) Cohen
707b3fc6b2 [DevTools] Make Toggle hover state more visible (#32914)
This change adds a background color to Toggles to make them easier to
see. This is especially important when DevTools are not in focus, and
it's harder to see.

Test plan:
1. `yarn build:chrome:local`
2. Inspect components 
3. Hover over "Select an Element in page to inspect it"
4. Observe background change
2025-04-15 11:20:29 +01:00
Piotr Tomczewski
7ff4d057b6 [DevTools] feat: show changed hooks names in the Profiler tab (#31398)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

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

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

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

## Summary

This PR adds support for displaying the names of changed hooks directly
in the Profiler tab, making it easier to identify specific updates.

A `HookChangeSummary` component has been introduced to show these hook
names, with a `displayMode` prop that toggles between `“compact”` for
tooltips and `“detailed”` for more in-depth views. This keeps tooltip
summaries concise while allowing for a full breakdown where needed.

This functionality also respects the `“Always parse hook names from
source”` setting from the Component inspector, as it uses the same
caching mechanism already in place for the Components tab. Additionally,
even without hook names parsed, the Profiler will now display hook types
(like `State`, `Callback`, etc.) based on data from `inspectedElement`.

To enable this across the DevTools, `InspectedElementContext` has been
moved higher in the component tree, allowing it to be shared between the
Profiler and Components tabs. This update allows hook name data to be
reused across tabs without duplication.

Additionally, a `getAlreadyLoadedHookNames` helper function was added to
efficiently access cached hook names, reducing the need for repeated
fetching when displaying changes.

These changes improve the ability to track specific hook updates within
the Profiler tab, making it clearer to see what’s changed.

### Before
Previously, the Profiler tab displayed only the IDs of changed hooks, as
shown below:
<img width="350" alt="Screenshot 2024-11-01 at 12 02 21_cropped"
src="https://github.com/user-attachments/assets/7a5f5f67-f1c8-4261-9ba3-1c76c9a88af3">

### After (without hook names parsed)
When hook names aren’t parsed, custom hooks and hook types are displayed
based on the inspectedElement data:
<img width="350" alt="Screenshot 2024-11-01 at 12 03 09_cropped"
src="https://github.com/user-attachments/assets/ed857a6d-e6ef-4e5b-982c-bf30c2d8a7e2">

### After (with hook names parsed)
Once hook names are fully parsed, the Profiler tab provides a complete
breakdown of specific hooks that have changed:
<img width="350" alt="Screenshot 2024-11-01 at 12 03 14_cropped"
src="https://github.com/user-attachments/assets/1ddfcc35-7474-4f4d-a084-f4e9f993a5bf">

This should resolve #21856 🎉
2025-04-15 11:10:00 +01:00
lauren
08075929f2 [compiler] Init react-mcp-server (#32859)
Just trying this out as a small hack for fun. Nothing serious is
planned.

Inits an MCP server that has 1 assistant prompt and two capabilities.
2025-04-14 18:39:00 -04:00
lauren
4eea4fcf41 [compiler] Update rimraf (#32868)
Just updating the compiler workspace package.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32868).
* #32859
* __->__ #32868
2025-04-14 15:15:14 -04:00
166 changed files with 11152 additions and 1099 deletions

View File

@@ -1,10 +1,11 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/scheduler"],
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "18",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",
"react-server-dom-webpack": "build/oss-experimental/react-server-dom-webpack",
"scheduler": "build/oss-experimental/scheduler"
},
"sandboxes": ["new"],

View File

@@ -10,7 +10,19 @@ on:
permissions: {}
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer

View File

@@ -16,6 +16,9 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
secrets:
NPM_TOKEN:
required: true
@@ -55,4 +58,4 @@ jobs:
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag ${{ inputs.dist_tag }}
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}

View File

@@ -14,6 +14,9 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
permissions: {}
@@ -29,5 +32,6 @@ jobs:
release_channel: ${{ inputs.release_channel }}
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,23 +0,0 @@
name: (Compiler) Publish Prereleases Weekly
on:
schedule:
# At 10 minutes past 9:00 on Mon
- cron: 10 9 * * 1
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_beta:
name: Publish to beta channel
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ github.sha }}
release_channel: beta
dist_tag: beta
version_name: '19.0.0'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,7 +10,19 @@ on:
permissions: {}
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer

View File

@@ -2,6 +2,7 @@ name: (Shared) Label Core Team PRs
on:
pull_request_target:
types: [opened]
permissions: {}
@@ -11,7 +12,19 @@ env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
jobs:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer

View File

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

59
compiler/CHANGELOG.md Normal file
View File

@@ -0,0 +1,59 @@
## 19.1.0-rc.1 (April 21, 2025)
## eslint-plugin-react-hooks
* Temporarily disable ref access in render validation [#32839](https://github.com/facebook/react/pull/32839) by [@poteto](https://github.com/poteto)
* Fix type error with recommended config [#32666](https://github.com/facebook/react/pull/32666) by [@niklasholm](https://github.com/niklasholm)
* Merge rule from eslint-plugin-react-compiler into `react-hooks` plugin [#32416](https://github.com/facebook/react/pull/32416) by [@michaelfaith](https://github.com/michaelfaith)
* Add dev dependencies for typescript migration [#32279](https://github.com/facebook/react/pull/32279) by [@michaelfaith](https://github.com/michaelfaith)
* Support v9 context api [#32045](https://github.com/facebook/react/pull/32045) by [@michaelfaith](https://github.com/michaelfaith)
* Support eslint 8+ flat plugin syntax out of the box for eslint-plugin-react-compiler [#32120](https://github.com/facebook/react/pull/32120) by [@orta](https://github.com/orta)
## babel-plugin-react-compiler
* Support satisfies operator [#32742](https://github.com/facebook/react/pull/32742) by [@rodrigofariow](https://github.com/rodrigofariow)
* Fix inferEffectDependencies lint false positives [#32769](https://github.com/facebook/react/pull/32769) by [@mofeiZ](https://github.com/mofeiZ)
* Fix hoisting of let declarations [#32724](https://github.com/facebook/react/pull/32724) by [@mofeiZ](https://github.com/mofeiZ)
* Avoid failing builds when import specifiers conflict or shadow vars [#32663](https://github.com/facebook/react/pull/32663) by [@mofeiZ](https://github.com/mofeiZ)
* Optimize components declared with arrow function and implicit return and `compilationMode: 'infer'` [#31792](https://github.com/facebook/react/pull/31792) by [@dimaMachina](https://github.com/dimaMachina)
* Validate static components [#32683](https://github.com/facebook/react/pull/32683) by [@josephsavona](https://github.com/josephsavona)
* Hoist dependencies from functions more conservatively [#32616](https://github.com/facebook/react/pull/32616) by [@mofeiZ](https://github.com/mofeiZ)
* Implement NumericLiteral as ObjectPropertyKey [#31791](https://github.com/facebook/react/pull/31791) by [@dimaMachina](https://github.com/dimaMachina)
* Avoid bailouts when inserting gating [#32598](https://github.com/facebook/react/pull/32598) by [@mofeiZ](https://github.com/mofeiZ)
* Stop bailing out early for hoisted gated functions [#32597](https://github.com/facebook/react/pull/32597) by [@mofeiZ](https://github.com/mofeiZ)
* Add shape for Array.from [#32522](https://github.com/facebook/react/pull/32522) by [@mofeiZ](https://github.com/mofeiZ)
* Patch array and argument spread mutability [#32521](https://github.com/facebook/react/pull/32521) by [@mofeiZ](https://github.com/mofeiZ)
* Make CompilerError compatible with reflection [#32539](https://github.com/facebook/react/pull/32539) by [@poteto](https://github.com/poteto)
* Add simple walltime measurement [#32331](https://github.com/facebook/react/pull/32331) by [@poteto](https://github.com/poteto)
* Improve error messages for unhandled terminal and instruction kinds [#32324](https://github.com/facebook/react/pull/32324) by [@inottn](https://github.com/inottn)
* Handle TSInstantiationExpression in lowerExpression [#32302](https://github.com/facebook/react/pull/32302) by [@inottn](https://github.com/inottn)
* Fix invalid Array.map type [#32095](https://github.com/facebook/react/pull/32095) by [@mofeiZ](https://github.com/mofeiZ)
* Patch for JSX escape sequences in @babel/generator [#32131](https://github.com/facebook/react/pull/32131) by [@mofeiZ](https://github.com/mofeiZ)
* `JSXText` emits incorrect with bracket [#32138](https://github.com/facebook/react/pull/32138) by [@himself65](https://github.com/himself65)
* Validation against calling impure functions [#31960](https://github.com/facebook/react/pull/31960) by [@josephsavona](https://github.com/josephsavona)
* Always target node [#32091](https://github.com/facebook/react/pull/32091) by [@poteto](https://github.com/poteto)
* Patch compilationMode:infer object method edge case [#32055](https://github.com/facebook/react/pull/32055) by [@mofeiZ](https://github.com/mofeiZ)
* Generate ts defs [#31994](https://github.com/facebook/react/pull/31994) by [@poteto](https://github.com/poteto)
* Relax react peer dep requirement [#31915](https://github.com/facebook/react/pull/31915) by [@poteto](https://github.com/poteto)
* Allow type cast expressions with refs [#31871](https://github.com/facebook/react/pull/31871) by [@josephsavona](https://github.com/josephsavona)
* Add shape for global Object.keys [#31583](https://github.com/facebook/react/pull/31583) by [@mofeiZ](https://github.com/mofeiZ)
* Optimize method calls w props receiver [#31775](https://github.com/facebook/react/pull/31775) by [@josephsavona](https://github.com/josephsavona)
* Fix dropped ref with spread props in InlineJsxTransform [#31726](https://github.com/facebook/react/pull/31726) by [@jackpope](https://github.com/jackpope)
* Support for non-declatation for in/of iterators [#31710](https://github.com/facebook/react/pull/31710) by [@mvitousek](https://github.com/mvitousek)
* Support for context variable loop iterators [#31709](https://github.com/facebook/react/pull/31709) by [@mvitousek](https://github.com/mvitousek)
* Replace deprecated dependency in `eslint-plugin-react-compiler` [#31629](https://github.com/facebook/react/pull/31629) by [@rakleed](https://github.com/rakleed)
* Support enableRefAsProp in jsx transform [#31558](https://github.com/facebook/react/pull/31558) by [@jackpope](https://github.com/jackpope)
* Fix: ref.current now correctly reactive [#31521](https://github.com/facebook/react/pull/31521) by [@mofeiZ](https://github.com/mofeiZ)
* Outline JSX with non-jsx children [#31442](https://github.com/facebook/react/pull/31442) by [@gsathya](https://github.com/gsathya)
* Outline jsx with duplicate attributes [#31441](https://github.com/facebook/react/pull/31441) by [@gsathya](https://github.com/gsathya)
* Store original and new prop names [#31440](https://github.com/facebook/react/pull/31440) by [@gsathya](https://github.com/gsathya)
* Stabilize compiler output: sort deps and decls by name [#31362](https://github.com/facebook/react/pull/31362) by [@mofeiZ](https://github.com/mofeiZ)
* Bugfix for hoistable deps for nested functions [#31345](https://github.com/facebook/react/pull/31345) by [@mofeiZ](https://github.com/mofeiZ)
* Remove compiler runtime-compat fixture library [#31430](https://github.com/facebook/react/pull/31430) by [@poteto](https://github.com/poteto)
* Wrap inline jsx transform codegen in conditional [#31267](https://github.com/facebook/react/pull/31267) by [@jackpope](https://github.com/jackpope)
* Check if local identifier is a hook when resolving globals [#31384](https://github.com/facebook/react/pull/31384) by [@poteto](https://github.com/poteto)
* Handle member expr as computed property [#31344](https://github.com/facebook/react/pull/31344) by [@gsathya](https://github.com/gsathya)
* Fix to ref access check to ban ref?.current [#31360](https://github.com/facebook/react/pull/31360) by [@mvitousek](https://github.com/mvitousek)
* InlineJSXTransform transforms jsx inside function expressions [#31282](https://github.com/facebook/react/pull/31282) by [@josephsavona](https://github.com/josephsavona)
## Other
* Add shebang to banner [#32225](https://github.com/facebook/react/pull/32225) by [@Jeremy-Hibiki](https://github.com/Jeremy-Hibiki)
* remove terser from react-compiler-runtime build [#31326](https://github.com/facebook/react/pull/31326) by [@henryqdineen](https://github.com/henryqdineen)

View File

@@ -27,7 +27,7 @@
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.42.1",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",

View File

@@ -781,12 +781,12 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.42.1":
version "1.47.2"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.47.2.tgz#dbe7051336bfc5cc599954214f9111181dbc7475"
integrity sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==
"@playwright/test@^1.51.1":
version "1.51.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
dependencies:
playwright "1.47.2"
playwright "1.51.1"
"@rtsao/scc@^1.1.0":
version "1.1.0"
@@ -1249,14 +1249,14 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001579:
version "1.0.30001669"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3"
integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==
version "1.0.30001715"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz"
integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==
caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663:
version "1.0.30001664"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz#d588d75c9682d3301956b05a3749652a80677df4"
integrity sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==
version "1.0.30001715"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz"
integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==
chalk@^2.4.2:
version "2.4.2"
@@ -3008,17 +3008,17 @@ pirates@^4.0.1:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
playwright-core@1.47.2:
version "1.47.2"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.2.tgz#7858da9377fa32a08be46ba47d7523dbd9460a4e"
integrity sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==
playwright-core@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
playwright@1.47.2:
version "1.47.2"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.2.tgz#155688aa06491ee21fb3e7555b748b525f86eb20"
integrity sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==
playwright@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
dependencies:
playwright-core "1.47.2"
playwright-core "1.51.1"
optionalDependencies:
fsevents "2.3.2"

View File

@@ -37,7 +37,7 @@
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.26.0",
"prompt-promise": "^1.0.3",
"rimraf": "^5.0.10",
"rimraf": "^6.0.1",
"to-fast-properties": "^2.0.0",
"tsup": "^8.4.0",
"typescript": "^5.4.3",
@@ -45,7 +45,6 @@
"yargs": "^17.7.2"
},
"resolutions": {
"rimraf": "5.0.10",
"@babel/types": "7.26.3"
},
"packageManager": "yarn@1.22.22"

View File

@@ -182,7 +182,9 @@ export type LoggerEvent =
| CompileDiagnosticEvent
| CompileSkipEvent
| PipelineErrorEvent
| TimingEvent;
| TimingEvent
| AutoDepsDecorationsEvent
| AutoDepsEligibleEvent;
export type CompileErrorEvent = {
kind: 'CompileError';
@@ -219,6 +221,16 @@ export type TimingEvent = {
kind: 'Timing';
measurement: PerformanceMeasure;
};
export type AutoDepsDecorationsEvent = {
kind: 'AutoDepsDecorations';
fnLoc: t.SourceLocation;
decorations: Array<t.SourceLocation>;
};
export type AutoDepsEligibleEvent = {
kind: 'AutoDepsEligible';
fnLoc: t.SourceLocation;
depArrayLoc: t.SourceLocation;
};
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;

View File

@@ -392,6 +392,11 @@ function runWithEnvironment(
if (env.config.inferEffectDependencies) {
inferEffectDependencies(hir);
log({
kind: 'hir',
name: 'InferEffectDependencies',
value: hir,
});
}
if (env.config.inlineJsxTransform) {

View File

@@ -469,6 +469,23 @@ export function compileProgram(
}
}
/**
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
* unused 'use no forget/memo' directive.
*/
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
for (const directive of optOutDirectives) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSkip',
fnLoc: fn.node.body.loc ?? null,
reason: `Skipped due to '${directive.value.value}' directive.`,
loc: directive.loc ?? null,
});
}
return null;
}
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSuccess',
fnLoc: fn.node.loc ?? null,
@@ -492,23 +509,6 @@ export function compileProgram(
return null;
}
/**
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
* unused 'use no forget/memo' directive.
*/
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
for (const directive of optOutDirectives) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSkip',
fnLoc: fn.node.body.loc ?? null,
reason: `Skipped due to '${directive.value.value}' directive.`,
loc: directive.loc ?? null,
});
}
return null;
}
if (!pass.opts.noEmit) {
return compileResult.compiledFn;
}

View File

@@ -1,3 +1,10 @@
/**
* 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 type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {PluginOptions} from './Options';

View File

@@ -1,3 +1,10 @@
/**
* 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 {NodePath} from '@babel/core';
import * as t from '@babel/types';

View File

@@ -1,3 +1,10 @@
/**
* 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} from '..';
import {
BlockId,

View File

@@ -1,3 +1,10 @@
/**
* 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} from '../CompilerError';
import {getScopes, recursivelyTraverseItems} from './AssertValidBlockNesting';
import {Environment} from './Environment';

View File

@@ -1,3 +1,10 @@
/**
* 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} from '../CompilerError';
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
@@ -12,6 +19,7 @@ import {
BasicBlock,
BlockId,
DependencyPathEntry,
FunctionExpression,
GeneratedSource,
getHookKind,
HIRFunction,
@@ -23,6 +31,7 @@ import {
PropertyLiteral,
ReactiveScopeDependency,
ScopeId,
TInstruction,
} from './HIR';
const DEBUG_PRINT = false;
@@ -120,6 +129,33 @@ export function collectHoistablePropertyLoads(
});
}
export function collectHoistablePropertyLoadsInInnerFn(
fnInstr: TInstruction<FunctionExpression>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
): ReadonlyMap<BlockId, BlockInfo> {
const fn = fnInstr.value.loweredFunc.func;
const initialContext: CollectHoistablePropertyLoadsContext = {
temporaries,
knownImmutableIdentifiers: new Set(),
hoistableFromOptionals,
registry: new PropertyPathRegistry(),
nestedFnImmutableContext: null,
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
? new Set()
: getAssumedInvokedFunctions(fn),
};
const nestedFnImmutableContext = new Set(
fn.context
.filter(place =>
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
)
.map(place => place.identifier.id),
);
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
return collectHoistablePropertyLoadsImpl(fn, initialContext);
}
type CollectHoistablePropertyLoadsContext = {
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;

View File

@@ -1,3 +1,10 @@
/**
* 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} from '..';
import {assertNonNull} from './CollectHoistablePropertyLoads';
import {

View File

@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInFireFunctionId,
BuiltInFireId,
BuiltInMapId,
BuiltInMixedReadonlyId,
@@ -674,7 +675,12 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
{
positionalParams: [],
restParam: null,
returnType: {kind: 'Primitive'},
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInFireFunctionId,
isConstructor: false,
},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
},

View File

@@ -1722,6 +1722,12 @@ export function isDispatcherType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInDispatch';
}
export function isFireFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInFireFunction'
);
}
export function isStableType(id: Identifier): boolean {
return (
isSetStateType(id) ||
@@ -1732,6 +1738,40 @@ export function isStableType(id: Identifier): boolean {
);
}
export function isStableTypeContainer(id: Identifier): boolean {
const type_ = id.type;
if (type_.kind !== 'Object') {
return false;
}
return (
isUseStateType(id) || // setState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
export function evaluatesToStableTypeOrContainer(
env: Environment,
{value}: Instruction,
): boolean {
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const calleeHookKind = getHookKind(env, callee.identifier);
switch (calleeHookKind) {
case 'useState':
case 'useReducer':
case 'useActionState':
case 'useRef':
case 'useTransition':
return true;
}
}
return false;
}
export function isUseEffectHookType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'

View File

@@ -1,3 +1,10 @@
/**
* 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,
InstructionId,

View File

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

View File

@@ -1,3 +1,10 @@
/**
* 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 {
ScopeId,
HIRFunction,
@@ -109,7 +116,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
}
}
function findTemporariesUsedOutsideDeclaringScope(
export function findTemporariesUsedOutsideDeclaringScope(
fn: HIRFunction,
): ReadonlySet<DeclarationId> {
/*
@@ -371,7 +378,7 @@ type Decl = {
scope: Stack<ReactiveScope>;
};
class Context {
export class DependencyCollectionContext {
#declarations: Map<DeclarationId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
@@ -638,7 +645,10 @@ enum HIRValue {
Terminal,
}
function handleInstruction(instr: Instruction, context: Context): void {
export function handleInstruction(
instr: Instruction,
context: DependencyCollectionContext,
): void {
const {id, value, lvalue} = instr;
context.declare(lvalue.identifier, {
id,
@@ -701,7 +711,7 @@ function collectDependencies(
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new Context(
const context = new DependencyCollectionContext(
usedOutsideDeclaringScope,
temporaries,
processedInstrsInOptional,

View File

@@ -1,3 +1,10 @@
/**
* 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} from '..';
import {BlockId, GotoVariant, HIRFunction} from './HIR';

View File

@@ -1,3 +1,11 @@
/**
* 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 * as t from '@babel/types';
import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
@@ -14,17 +22,30 @@ import {
ScopeId,
ReactiveScopeDependency,
Place,
ReactiveScope,
ReactiveScopeDependencies,
Terminal,
isUseRefType,
isSetStateType,
isFireFunctionType,
makeScopeId,
} from '../HIR';
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
} from '../HIR/HIRBuilder';
import {
collectTemporariesSidemap,
DependencyCollectionContext,
handleInstruction,
} from '../HIR/PropagateScopeDependenciesHIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {empty} from '../Utils/Stack';
import {getOrInsertWith} from '../Utils/utils';
/**
@@ -53,10 +74,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
const autodepFnLoads = new Map<IdentifierId, number>();
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
const scopeInfos = new Map<
ScopeId,
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
>();
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
const loadGlobals = new Set<IdentifierId>();
@@ -70,19 +88,18 @@ export function inferEffectDependencies(fn: HIRFunction): void {
const reactiveIds = inferReactiveIdentifiers(fn);
for (const [, block] of fn.body.blocks) {
if (
block.terminal.kind === 'scope' ||
block.terminal.kind === 'pruned-scope'
) {
if (block.terminal.kind === 'scope') {
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
scopeInfos.set(block.terminal.scope.id, {
pruned: block.terminal.kind === 'pruned-scope',
deps: block.terminal.scope.dependencies,
hasSingleInstr:
scopeBlock.instructions.length === 1 &&
scopeBlock.terminal.kind === 'goto' &&
scopeBlock.terminal.block === block.terminal.fallthrough,
});
if (
scopeBlock.instructions.length === 1 &&
scopeBlock.terminal.kind === 'goto' &&
scopeBlock.terminal.block === block.terminal.fallthrough
) {
scopeInfos.set(
block.terminal.scope.id,
block.terminal.scope.dependencies,
);
}
}
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
for (const instr of block.instructions) {
@@ -164,22 +181,12 @@ export function inferEffectDependencies(fn: HIRFunction): void {
fnExpr.lvalue.identifier.scope != null
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
: null;
CompilerError.invariant(scopeInfo != null, {
reason: 'Expected function expression scope to exist',
loc: value.loc,
});
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
/**
* TODO: retry pipeline that ensures effect function expressions
* are placed into their own scope
*/
CompilerError.throwTodo({
reason:
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
loc: fnExpr.loc,
});
let minimalDeps: Set<ReactiveScopeDependency>;
if (scopeInfo != null) {
minimalDeps = new Set(scopeInfo);
} else {
minimalDeps = inferMinimalDependencies(fnExpr);
}
/**
* Step 1: push dependencies to the effect deps array
*
@@ -187,11 +194,14 @@ export function inferEffectDependencies(fn: HIRFunction): void {
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
* explanation.
*/
for (const dep of scopeInfo.deps) {
const usedDeps = [];
for (const dep of minimalDeps) {
if (
(isUseRefType(dep.identifier) ||
((isUseRefType(dep.identifier) ||
isSetStateType(dep.identifier)) &&
!reactiveIds.has(dep.identifier.id)
!reactiveIds.has(dep.identifier.id)) ||
isFireFunctionType(dep.identifier)
) {
// exclude non-reactive hook results, which will never be in a memo block
continue;
@@ -205,6 +215,23 @@ export function inferEffectDependencies(fn: HIRFunction): void {
);
newInstructions.push(...instructions);
effectDeps.push(place);
usedDeps.push(dep);
}
// For LSP autodeps feature.
const decorations: Array<t.SourceLocation> = [];
for (const loc of collectDepUsages(usedDeps, fnExpr.value)) {
if (typeof loc === 'symbol') {
continue;
}
decorations.push(loc);
}
if (typeof value.loc !== 'symbol') {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'AutoDepsDecorations',
fnLoc: value.loc,
decorations,
});
}
newInstructions.push({
@@ -230,6 +257,31 @@ export function inferEffectDependencies(fn: HIRFunction): void {
rewriteInstrs.set(instr.id, newInstructions);
fn.env.inferredEffectLocations.add(callee.loc);
}
} else if (
value.args.length >= 2 &&
value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
value.args[0] != null &&
value.args[0].kind === 'Identifier'
) {
const penultimateArg = value.args[value.args.length - 2];
const depArrayArg = value.args[value.args.length - 1];
if (
depArrayArg.kind !== 'Spread' &&
penultimateArg.kind !== 'Spread' &&
typeof depArrayArg.loc !== 'symbol' &&
typeof penultimateArg.loc !== 'symbol' &&
typeof value.loc !== 'symbol'
) {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'AutoDepsEligible',
fnLoc: value.loc,
depArrayLoc: {
...depArrayArg.loc,
start: penultimateArg.loc.end,
end: depArrayArg.loc.end,
},
});
}
}
}
}
@@ -338,3 +390,163 @@ function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
}
return reactiveIds;
}
function collectDepUsages(
deps: Array<ReactiveScopeDependency>,
fnExpr: FunctionExpression,
): Array<SourceLocation> {
const identifiers: Map<IdentifierId, ReactiveScopeDependency> = new Map();
const loadedDeps: Set<IdentifierId> = new Set();
const sourceLocations = [];
for (const dep of deps) {
identifiers.set(dep.identifier.id, dep);
}
for (const [, block] of fnExpr.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'LoadLocal' &&
identifiers.has(instr.value.place.identifier.id)
) {
loadedDeps.add(instr.lvalue.identifier.id);
}
for (const place of eachInstructionOperand(instr)) {
if (loadedDeps.has(place.identifier.id)) {
// TODO(@jbrown215): handle member exprs!!
sourceLocations.push(place.identifier.loc);
}
}
}
}
return sourceLocations;
}
function inferMinimalDependencies(
fnInstr: TInstruction<FunctionExpression>,
): Set<ReactiveScopeDependency> {
const fn = fnInstr.value.loweredFunc.func;
const temporaries = collectTemporariesSidemap(fn, new Set());
const {
hoistableObjects,
processedInstrsInOptional,
temporariesReadInOptional,
} = collectOptionalChainSidemap(fn);
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
fnInstr,
temporaries,
hoistableObjects,
);
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
CompilerError.invariant(hoistableToFnEntry != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing entry block',
loc: fnInstr.loc,
});
const dependencies = inferDependencies(
fnInstr,
new Map([...temporaries, ...temporariesReadInOptional]),
processedInstrsInOptional,
);
const tree = new ReactiveScopeDependencyTreeHIR(
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
);
for (const dep of dependencies) {
tree.addDependency({...dep});
}
return tree.deriveMinimalDependencies();
}
function inferDependencies(
fnInstr: TInstruction<FunctionExpression>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Set<ReactiveScopeDependency> {
const fn = fnInstr.value.loweredFunc.func;
const context = new DependencyCollectionContext(
new Set(),
temporaries,
processedInstrsInOptional,
);
for (const dep of fn.context) {
context.declare(dep.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
}
const placeholderScope: ReactiveScope = {
id: makeScopeId(0),
range: {
start: fnInstr.id,
end: makeInstructionId(fnInstr.id + 1),
},
dependencies: new Set(),
reassignments: new Set(),
declarations: new Map(),
earlyReturnValue: null,
merged: new Set(),
loc: GeneratedSource,
};
context.enterScope(placeholderScope);
inferDependenciesInFn(fn, context, temporaries);
context.exitScope(placeholderScope, false);
const resultUnfiltered = context.deps.get(placeholderScope);
CompilerError.invariant(resultUnfiltered != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
loc: fn.loc,
});
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
const result = new Set<ReactiveScopeDependency>();
for (const dep of resultUnfiltered) {
if (fnContext.has(dep.identifier.id)) {
result.add(dep);
}
}
return result;
}
function inferDependenciesInFn(
fn: HIRFunction,
context: DependencyCollectionContext,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
): void {
for (const [, block] of fn.body.blocks) {
// Record referenced optional chains in phis
for (const phi of block.phis) {
for (const operand of phi.operands) {
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
if (maybeOptionalChain) {
context.visitDependency(maybeOptionalChain);
}
}
}
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
context.declare(instr.lvalue.identifier, {
id: instr.id,
scope: context.currentScope,
});
/**
* Recursively visit the inner function to extract dependencies
*/
const innerFn = instr.value.loweredFunc.func;
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
inferDependenciesInFn(innerFn, context, temporaries);
});
} else {
handleInstruction(instr, context);
}
}
}
}

View File

@@ -9,14 +9,19 @@ import {CompilerError} from '..';
import {
BlockId,
Effect,
Environment,
HIRFunction,
Identifier,
IdentifierId,
Instruction,
Place,
computePostDominatorTree,
evaluatesToStableTypeOrContainer,
getHookKind,
isStableType,
isStableTypeContainer,
isUseOperator,
isUseRefType,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
@@ -31,6 +36,103 @@ import {
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
/**
* Side map to track and propagate sources of stability (i.e. hook calls such as
* `useRef()` and property reads such as `useState()[1]). Note that this
* requires forward data flow analysis since stability is not part of React
* Compiler's type system.
*/
class StableSidemap {
map: Map<IdentifierId, {isStable: boolean}> = new Map();
env: Environment;
constructor(env: Environment) {
this.env = env;
}
handleInstruction(instr: Instruction): void {
const {value, lvalue} = instr;
switch (value.kind) {
case 'CallExpression':
case 'MethodCall': {
/**
* Sources of stability are known hook calls
*/
if (evaluatesToStableTypeOrContainer(this.env, instr)) {
if (isStableType(lvalue.identifier)) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
} else {
this.map.set(lvalue.identifier.id, {
isStable: false,
});
}
} else if (
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
isUseRefType(lvalue.identifier)
) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
}
break;
}
case 'Destructure':
case 'PropertyLoad': {
/**
* PropertyLoads may from stable containers may also produce stable
* values. ComputedLoads are technically safe for now (as all stable
* containers have differently-typed elements), but are not handled as
* they should be rare anyways.
*/
const source =
value.kind === 'Destructure'
? value.value.identifier.id
: value.object.identifier.id;
const entry = this.map.get(source);
if (entry) {
for (const lvalue of eachInstructionLValue(instr)) {
if (isStableTypeContainer(lvalue.identifier)) {
this.map.set(lvalue.identifier.id, {
isStable: false,
});
} else if (isStableType(lvalue.identifier)) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
}
}
}
break;
}
case 'StoreLocal': {
const entry = this.map.get(value.value.identifier.id);
if (entry) {
this.map.set(lvalue.identifier.id, entry);
this.map.set(value.lvalue.place.identifier.id, entry);
}
break;
}
case 'LoadLocal': {
const entry = this.map.get(value.place.identifier.id);
if (entry) {
this.map.set(lvalue.identifier.id, entry);
}
break;
}
}
}
isStable(id: IdentifierId): boolean {
const entry = this.map.get(id);
return entry != null ? entry.isStable : false;
}
}
/*
* Infers which `Place`s are reactive, ie may *semantically* change
* over the course of the component/hook's lifetime. Places are reactive
@@ -111,6 +213,7 @@ import {assertExhaustive} from '../Utils/utils';
*/
export function inferReactivePlaces(fn: HIRFunction): void {
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
const stableIdentifierSources = new StableSidemap(fn.env);
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
reactiveIdentifiers.markReactive(place);
@@ -184,6 +287,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
}
for (const instruction of block.instructions) {
stableIdentifierSources.handleInstruction(instruction);
const {value} = instruction;
let hasReactiveInput = false;
/*
@@ -218,7 +322,13 @@ export function inferReactivePlaces(fn: HIRFunction): void {
if (hasReactiveInput) {
for (const lvalue of eachInstructionLValue(instruction)) {
if (isStableType(lvalue.identifier)) {
/**
* Note that it's not correct to mark all stable-typed identifiers
* as non-reactive, since ternaries and other value blocks can
* produce reactive identifiers typed as these.
* (e.g. `props.cond ? setState1 : setState2`)
*/
if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
continue;
}
reactiveIdentifiers.markReactive(lvalue);

View File

@@ -48,7 +48,7 @@ import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
import {assertExhaustive} from '../Utils/utils';
import {assertExhaustive, hasOwnProperty} from '../Utils/utils';
import {buildReactiveFunction} from './BuildReactiveFunction';
import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
@@ -374,6 +374,8 @@ function codegenReactiveFunction(
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
setMissingLocationsToNull(body);
return Ok({
type: 'CodegenFunction',
loc: fn.loc,
@@ -2665,3 +2667,38 @@ function compareScopeDeclaration(
else if (aName > bName) return 1;
else return 0;
}
function setMissingLocationsToNull(ast: any): void {
if (Array.isArray(ast)) {
ast.forEach(item => setMissingLocationsToNull(item));
return;
} else if (
ast == null ||
typeof ast !== 'object' ||
typeof ast['type'] !== 'string'
) {
return;
}
if (ast['loc'] == null) {
ast['loc'] = {
start: {line: null, column: null, index: null},
end: {line: null, column: null, index: null},
filename: null,
identifierName: null,
};
}
for (const key in ast) {
if (!hasOwnProperty(ast, key)) {
continue;
}
const value = ast[key];
if (typeof value !== 'object') {
/*
* We handle this above too, but avoid extra function calls in the majority of
* cases where we're traversing an AST node's properties
*/
continue;
}
setMissingLocationsToNull(ast[key]);
}
}

View File

@@ -1,3 +1,10 @@
/**
* 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 {
BlockId,
ReactiveFunction,

View File

@@ -34,7 +34,11 @@ import {
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {getOrInsertWith} from '../Utils/utils';
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
import {
BuiltInFireFunctionId,
BuiltInFireId,
DefaultNonmutatingHook,
} from '../HIR/ObjectShape';
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
@@ -633,6 +637,13 @@ class Context {
() => createTemporaryPlace(this.#env, GeneratedSource),
);
fireFunctionBinding.identifier.type = {
kind: 'Function',
shapeId: BuiltInFireFunctionId,
return: {kind: 'Poly'},
isConstructor: false,
};
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
fireFunctionBinding,
capturedCalleeIdentifier: callee.identifier,

View File

@@ -4,4 +4,5 @@
* 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

@@ -4,6 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';

View File

@@ -0,0 +1,39 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function Component({foo}) {
const arr = [];
// Taking either arr[0].value or arr as a dependency is reasonable
// as long as developers know what to expect.
useEffect(() => print(arr[0].value));
arr.push({value: foo});
return arr;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useEffect } from "react";
import { print } from "shared-runtime";
function Component(t0) {
const { foo } = t0;
const arr = [];
useEffect(() => print(arr[0].value), [arr[0].value]);
arr.push({ value: foo });
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,12 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
import {print} from 'shared-runtime';
function Component({foo}) {
const arr = [];
// Taking either arr[0].value or arr as a dependency is reasonable
// as long as developers know what to expect.
useEffect(() => print(arr[0].value));
arr.push({value: foo});
return arr;
}

View File

@@ -0,0 +1,38 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
function Component({arrRef}) {
// Avoid taking arr.current as a dependency
useEffect(() => print(arrRef.current));
arrRef.current.val = 2;
return arrRef;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useEffect, useRef } from "react";
import { print } from "shared-runtime";
function Component(t0) {
const { arrRef } = t0;
useEffect(() => print(arrRef.current), [arrRef]);
arrRef.current.val = 2;
return arrRef;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect, useRef} from 'react';
import {print} from 'shared-runtime';
function Component({arrRef}) {
// Avoid taking arr.current as a dependency
useEffect(() => print(arrRef.current));
arrRef.current.val = 2;
return arrRef;
}

View File

@@ -0,0 +1,34 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
function Component({foo}) {
const arr = [];
useEffect(() => arr.push(foo));
arr.push(2);
return arr;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useEffect } from "react";
function Component(t0) {
const { foo } = t0;
const arr = [];
useEffect(() => arr.push(foo), [arr, foo]);
arr.push(2);
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,9 @@
// @inferEffectDependencies @panicThreshold(none)
import {useEffect} from 'react';
function Component({foo}) {
const arr = [];
useEffect(() => arr.push(foo));
arr.push(2);
return arr;
}

View File

@@ -0,0 +1,69 @@
## Input
```javascript
// @inferEffectDependencies
import {useRef, useEffect} from 'react';
import {print, mutate} from 'shared-runtime';
function Component({cond}) {
const arr = useRef([]);
const other = useRef([]);
// Although arr and other are both stable, derived is not
const derived = cond ? arr : other;
useEffect(() => {
mutate(derived.current);
print(derived.current);
});
return arr;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useRef, useEffect } from "react";
import { print, mutate } from "shared-runtime";
function Component(t0) {
const $ = _c(4);
const { cond } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[0] = t1;
} else {
t1 = $[0];
}
const arr = useRef(t1);
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[1] = t2;
} else {
t2 = $[1];
}
const other = useRef(t2);
const derived = cond ? arr : other;
let t3;
if ($[2] !== derived) {
t3 = () => {
mutate(derived.current);
print(derived.current);
};
$[2] = derived;
$[3] = t3;
} else {
t3 = $[3];
}
useEffect(t3, [derived]);
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,15 @@
// @inferEffectDependencies
import {useRef, useEffect} from 'react';
import {print, mutate} from 'shared-runtime';
function Component({cond}) {
const arr = useRef([]);
const other = useRef([]);
// Although arr and other are both stable, derived is not
const derived = cond ? arr : other;
useEffect(() => {
mutate(derived.current);
print(derived.current);
});
return arr;
}

View File

@@ -83,10 +83,10 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
function Parent(t0) {
const $ = _c2(2);
const $ = _c2(3);
const { children, ref } = t0;
let t1;
if ($[0] !== children) {
if ($[0] !== children || $[1] !== ref) {
if (DEV) {
t1 = <div ref={ref}>{children}</div>;
} else {
@@ -99,9 +99,10 @@ function Parent(t0) {
};
}
$[0] = children;
$[1] = t1;
$[1] = ref;
$[2] = t1;
} else {
t1 = $[1];
t1 = $[2];
}
return t1;
}

View File

@@ -0,0 +1,51 @@
## Input
```javascript
// @sourceMaps
export const Button = () => {
return <button>Click me</button>;
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @sourceMaps
export const Button = () => {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <button>Click me</button>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
};
```
## Source Map
```
{
"version": 3,
"names": [
"Button",
"t0"
],
"sources": [
"sourcemaps-simple.ts"
],
"sourcesContent": [
"// @sourceMaps\nexport const Button = () => {\n return <button>Click me</button>;\n};\n"
],
"mappings": "kDAAA;AACA,OAAO,MAAMA,MAAM,GAAGA,CAAA,K;SACb,OAAyB,CAAjB,QAAQ,EAAhB,MAAyB,C,qCAAzBC,EAAyB,C,CACjC",
"ignoreList": []
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,4 @@
// @sourceMaps
export const Button = () => {
return <button>Click me</button>;
};

View File

@@ -1,42 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useRef} from 'react';
import {useSpecialEffect} from 'shared-runtime';
/**
* The retry pipeline disables memoization features, which means we need to
* provide an alternate implementation of effect dependencies which does not
* rely on memoization.
*/
function useFoo({cond}) {
const ref = useRef();
const derived = cond ? ref.current : makeObject();
useSpecialEffect(() => {
log(derived);
}, [derived]);
return ref;
}
```
## Error
```
11 | const ref = useRef();
12 | const derived = cond ? ref.current : makeObject();
> 13 | useSpecialEffect(() => {
| ^^^^^^^^^^^^^^^^^^^^^^^^
> 14 | log(derived);
| ^^^^^^^^^^^^^^^^^
> 15 | }, [derived]);
| ^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.. (Bailout reason: Invariant: Expected function expression scope to exist (13:15)) (13:15)
16 | return ref;
17 | }
18 |
```

View File

@@ -0,0 +1,54 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold(none)
import {useRef} from 'react';
import {useSpecialEffect} from 'shared-runtime';
/**
* The retry pipeline disables memoization features, which means we need to
* provide an alternate implementation of effect dependencies which does not
* rely on memoization.
*/
function useFoo({cond}) {
const ref = useRef();
const derived = cond ? ref.current : makeObject();
useSpecialEffect(() => {
log(derived);
}, [derived]);
return ref;
}
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold(none)
import { useRef } from "react";
import { useSpecialEffect } from "shared-runtime";
/**
* The retry pipeline disables memoization features, which means we need to
* provide an alternate implementation of effect dependencies which does not
* rely on memoization.
*/
function useFoo(t0) {
const { cond } = t0;
const ref = useRef();
const derived = cond ? ref.current : makeObject();
useSpecialEffect(
() => {
log(derived);
},
[derived],
[derived],
);
return ref;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -49,7 +49,7 @@ function Component(props) {
} else {
t2 = $[4];
}
useEffect(t2, [t1, props]);
useEffect(t2, [props]);
return null;
}

View File

@@ -1,3 +1,10 @@
/**
* 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 {defineConfig} from 'tsup';
export default defineConfig({

View File

@@ -1,3 +1,10 @@
/**
* 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 {defineConfig} from 'tsup';
export default defineConfig({

View File

@@ -1,3 +1,10 @@
/**
* 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 {defineConfig} from 'tsup';
export default defineConfig({

View File

@@ -1,3 +1,10 @@
/**
* 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 const config = {
knownIncompatibleLibraries: [
'mobx-react',

View File

@@ -1,3 +1,10 @@
/**
* 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 {defineConfig} from 'tsup';
export default defineConfig({

View File

@@ -1,3 +1,10 @@
/**
* 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 {defineConfig} from 'tsup';
export default defineConfig({

View File

@@ -0,0 +1,106 @@
/**
* 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 * as vscode from 'vscode';
import {
LanguageClient,
RequestType,
type Position,
} from 'vscode-languageclient/node';
import {positionLiteralToVSCodePosition, positionsToRange} from './mapping';
export type AutoDepsDecorationsLSPEvent = {
useEffectCallExpr: [Position, Position];
decorations: Array<[Position, Position]>;
};
export interface AutoDepsDecorationsParams {
position: Position;
}
export namespace AutoDepsDecorationsRequest {
export const type = new RequestType<
AutoDepsDecorationsParams,
AutoDepsDecorationsLSPEvent | null,
void
>('react/autodeps_decorations');
}
const inferredEffectDepDecoration =
vscode.window.createTextEditorDecorationType({
// TODO: make configurable?
borderColor: new vscode.ThemeColor('diffEditor.move.border'),
borderStyle: 'solid',
borderWidth: '0 0 4px 0',
});
let currentlyDecoratedAutoDepFnLoc: vscode.Range | null = null;
export function getCurrentlyDecoratedAutoDepFnLoc(): vscode.Range | null {
return currentlyDecoratedAutoDepFnLoc;
}
export function setCurrentlyDecoratedAutoDepFnLoc(range: vscode.Range): void {
currentlyDecoratedAutoDepFnLoc = range;
}
export function clearCurrentlyDecoratedAutoDepFnLoc(): void {
currentlyDecoratedAutoDepFnLoc = null;
}
let decorationRequestId = 0;
export type AutoDepsDecorationsOptions = {
shouldUpdateCurrent: boolean;
};
export function requestAutoDepsDecorations(
client: LanguageClient,
position: vscode.Position,
options: AutoDepsDecorationsOptions,
) {
const id = ++decorationRequestId;
client
.sendRequest(AutoDepsDecorationsRequest.type, {position})
.then(response => {
if (response !== null) {
const {
decorations,
useEffectCallExpr: [start, end],
} = response;
// Maintain ordering
if (decorationRequestId === id) {
if (options.shouldUpdateCurrent) {
setCurrentlyDecoratedAutoDepFnLoc(positionsToRange(start, end));
}
drawInferredEffectDepDecorations(decorations);
}
} else {
clearCurrentlyDecoratedAutoDepFnLoc();
clearDecorations(inferredEffectDepDecoration);
}
});
}
export function drawInferredEffectDepDecorations(
decorations: Array<[Position, Position]>,
): void {
const decorationOptions = decorations.map(([start, end]) => {
return {
range: new vscode.Range(
positionLiteralToVSCodePosition(start),
positionLiteralToVSCodePosition(end),
),
hoverMessage: 'Inferred as an effect dependency',
};
});
vscode.window.activeTextEditor?.setDecorations(
inferredEffectDepDecoration,
decorationOptions,
);
}
export function clearDecorations(
decorationType: vscode.TextEditorDecorationType,
) {
vscode.window.activeTextEditor?.setDecorations(decorationType, []);
}

View File

@@ -0,0 +1,80 @@
/**
* 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.
*/
type RGB = [number, number, number];
const int = Math.floor;
export class Color {
constructor(
private r: number,
private g: number,
private b: number,
) {}
toAlphaString(a: number) {
return this.toCssString(a);
}
toString() {
return this.toCssString(1);
}
/**
* Adjust the color by a multiplier to lighten (`> 1.0`) or darken (`< 1.0`) the color. Returns a new
* instance.
*/
adjusted(mult: number) {
const adjusted = Color.redistribute([
this.r * mult,
this.g * mult,
this.b * mult,
]);
return new Color(...adjusted);
}
private toCssString(a: number) {
return `rgba(${this.r},${this.g},${this.b},${a})`;
}
/**
* Redistributes rgb, maintaing hue until its clamped.
* https://stackoverflow.com/a/141943
*/
private static redistribute([r, g, b]: RGB): RGB {
const threshold = 255.999;
const max = Math.max(r, g, b);
if (max <= threshold) {
return [int(r), int(g), int(b)];
}
const total = r + g + b;
if (total >= 3 * threshold) {
return [int(threshold), int(threshold), int(threshold)];
}
const x = (3 * threshold - total) / (3 * max - total);
const gray = threshold - x * max;
return [int(gray + x * r), int(gray + x * g), int(gray + x * b)];
}
}
export const BLACK = new Color(0, 0, 0);
export const WHITE = new Color(255, 255, 255);
const COLOR_POOL = [
new Color(249, 65, 68),
new Color(243, 114, 44),
new Color(248, 150, 30),
new Color(249, 132, 74),
new Color(249, 199, 79),
new Color(144, 190, 109),
new Color(67, 170, 139),
new Color(77, 144, 142),
new Color(87, 117, 144),
new Color(39, 125, 161),
];
export function getColorFor(index: number): Color {
return COLOR_POOL[Math.abs(index) % COLOR_POOL.length]!;
}

View File

@@ -1,17 +1,34 @@
/**
* 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 * as path from 'path';
import {ExtensionContext, window as Window} from 'vscode';
import * as vscode from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
type Position,
ServerOptions,
TransportKind,
} from 'vscode-languageclient/node';
import {positionLiteralToVSCodePosition} from './mapping';
import {
getCurrentlyDecoratedAutoDepFnLoc,
requestAutoDepsDecorations,
} from './autodeps';
let client: LanguageClient;
export function activate(context: ExtensionContext) {
export function activate(context: vscode.ExtensionContext) {
const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
const documentSelector = [
{scheme: 'file', language: 'javascriptreact'},
{scheme: 'file', language: 'typescriptreact'},
];
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
@@ -27,10 +44,7 @@ export function activate(context: ExtensionContext) {
};
const clientOptions: LanguageClientOptions = {
documentSelector: [
{scheme: 'file', language: 'javascriptreact'},
{scheme: 'file', language: 'typescriptreact'},
],
documentSelector,
progressOnInitialization: true,
};
@@ -43,12 +57,39 @@ export function activate(context: ExtensionContext) {
clientOptions,
);
} catch {
Window.showErrorMessage(
vscode.window.showErrorMessage(
`React Analyzer couldn't be started. See the output channel for details.`,
);
return;
}
vscode.languages.registerHoverProvider(documentSelector, {
provideHover(_document, position, _token) {
requestAutoDepsDecorations(client, position, {shouldUpdateCurrent: true});
return null;
},
});
vscode.workspace.onDidChangeTextDocument(async _e => {
const currentlyDecoratedAutoDepFnLoc = getCurrentlyDecoratedAutoDepFnLoc();
if (currentlyDecoratedAutoDepFnLoc !== null) {
requestAutoDepsDecorations(client, currentlyDecoratedAutoDepFnLoc.start, {
shouldUpdateCurrent: false,
});
}
});
vscode.commands.registerCommand(
'react.requestAutoDepsDecorations',
(position: Position) => {
requestAutoDepsDecorations(
client,
positionLiteralToVSCodePosition(position),
{shouldUpdateCurrent: true},
);
},
);
client.registerProposedFeatures();
client.start();
}
@@ -57,4 +98,5 @@ export function deactivate(): Thenable<void> | undefined {
if (client !== undefined) {
return client.stop();
}
return;
}

View File

@@ -0,0 +1,22 @@
/**
* 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 * as vscode from 'vscode';
import {Position} from 'vscode-languageclient/node';
export function positionLiteralToVSCodePosition(
position: Position,
): vscode.Position {
return new vscode.Position(position.line, position.character);
}
export function positionsToRange(start: Position, end: Position): vscode.Range {
return new vscode.Range(
positionLiteralToVSCodePosition(start),
positionLiteralToVSCodePosition(end),
);
}

View File

@@ -1,3 +1,10 @@
/**
* 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 {SourceLocation} from 'babel-plugin-react-compiler/src';
import {type Range} from 'vscode-languageserver';

View File

@@ -33,6 +33,8 @@ export async function compile({
plugins: ['typescript', 'jsx'],
},
sourceType: 'module',
configFile: false,
babelrc: false,
});
if (ast == null) {
return null;
@@ -48,6 +50,8 @@ export async function compile({
plugins,
sourceType: 'module',
sourceFileName: file,
configFile: false,
babelrc: false,
});
if (result?.code == null) {
throw new Error(

View File

@@ -7,10 +7,14 @@
import {TextDocument} from 'vscode-languageserver-textdocument';
import {
CodeAction,
CodeActionKind,
CodeLens,
Command,
createConnection,
type InitializeParams,
type InitializeResult,
Position,
ProposedFeatures,
TextDocuments,
TextDocumentSyncKind,
@@ -19,11 +23,22 @@ import {compile, lastResult} from './compiler';
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
import {resolveReactConfig} from './compiler/options';
import {
CompileSuccessEvent,
type CompileSuccessEvent,
type LoggerEvent,
defaultOptions,
LoggerEvent,
} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
import {
type AutoDepsDecorationsLSPEvent,
AutoDepsDecorationsRequest,
mapCompilerEventToLSPEvent,
} from './requests/autodepsdecorations';
import {
isPositionWithinRange,
isRangeWithinRange,
Range,
sourceLocationToRange,
} from './utils/range';
const SUPPORTED_LANGUAGE_IDS = new Set([
'javascript',
@@ -37,17 +52,68 @@ const documents = new TextDocuments(TextDocument);
let compilerOptions: PluginOptions | null = null;
let compiledFns: Set<CompileSuccessEvent> = new Set();
let autoDepsDecorations: Array<AutoDepsDecorationsLSPEvent> = [];
let codeActionEvents: Array<CodeActionLSPEvent> = [];
type CodeActionLSPEvent = {
title: string;
kind: CodeActionKind;
newText: string;
anchorRange: Range;
editRange: {start: Position; end: Position};
};
connection.onInitialize((_params: InitializeParams) => {
// TODO(@poteto) get config fr
compilerOptions = resolveReactConfig('.') ?? defaultOptions;
compilerOptions = {
...compilerOptions,
environment: {
...compilerOptions.environment,
inferEffectDependencies: [
{
function: {
importSpecifierName: 'useEffect',
source: 'react',
},
numRequiredArgs: 1,
},
{
function: {
importSpecifierName: 'useSpecialEffect',
source: 'shared-runtime',
},
numRequiredArgs: 2,
},
{
function: {
importSpecifierName: 'default',
source: 'useEffectWrapper',
},
numRequiredArgs: 1,
},
],
},
logger: {
logEvent(_filename: string | null, event: LoggerEvent) {
connection.console.info(`Received event: ${event.kind}`);
connection.console.debug(JSON.stringify(event, null, 2));
if (event.kind === 'CompileSuccess') {
compiledFns.add(event);
}
if (event.kind === 'AutoDepsDecorations') {
autoDepsDecorations.push(mapCompilerEventToLSPEvent(event));
}
if (event.kind === 'AutoDepsEligible') {
const depArrayLoc = sourceLocationToRange(event.depArrayLoc);
codeActionEvents.push({
title: 'Use React Compiler inferred dependency array',
kind: CodeActionKind.QuickFix,
newText: '',
anchorRange: sourceLocationToRange(event.fnLoc),
editRange: {start: depArrayLoc[0], end: depArrayLoc[1]},
});
}
},
},
};
@@ -55,6 +121,7 @@ connection.onInitialize((_params: InitializeParams) => {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
codeLensProvider: {resolveProvider: true},
codeActionProvider: {resolveProvider: true},
},
};
return result;
@@ -65,20 +132,29 @@ connection.onInitialized(() => {
});
documents.onDidChangeContent(async event => {
connection.console.info(`Changed: ${event.document.uri}`);
compiledFns.clear();
connection.console.info(`Compiling: ${event.document.uri}`);
resetState();
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
const text = event.document.getText();
await compile({
text,
file: event.document.uri,
options: compilerOptions,
});
try {
await compile({
text,
file: event.document.uri,
options: compilerOptions,
});
} catch (err) {
connection.console.error('Failed to compile');
if (err instanceof Error) {
connection.console.error(err.stack ?? err.message);
} else {
connection.console.error(JSON.stringify(err, null, 2));
}
}
}
});
connection.onDidChangeWatchedFiles(change => {
compiledFns.clear();
resetState();
connection.console.log(
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
);
@@ -118,6 +194,62 @@ connection.onCodeLensResolve(lens => {
return lens;
});
connection.onCodeAction(params => {
const codeActions: Array<CodeAction> = [];
for (const codeActionEvent of codeActionEvents) {
if (
isRangeWithinRange(
[params.range.start, params.range.end],
codeActionEvent.anchorRange,
)
) {
const codeAction = CodeAction.create(
codeActionEvent.title,
{
changes: {
[params.textDocument.uri]: [
{
newText: codeActionEvent.newText,
range: codeActionEvent.editRange,
},
],
},
},
codeActionEvent.kind,
);
// After executing a codeaction, we want to draw autodep decorations again
codeAction.command = Command.create(
'Request autodeps decorations',
'react.requestAutoDepsDecorations',
codeActionEvent.anchorRange[0],
);
codeActions.push(codeAction);
}
}
return codeActions;
});
/**
* The client can request the server to compute autodeps decorations based on a currently selected
* position if the selected position is within an autodep eligible function call.
*/
connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
const position = params.position;
for (const decoration of autoDepsDecorations) {
if (isPositionWithinRange(position, decoration.useEffectCallExpr)) {
return decoration;
}
}
return null;
});
function resetState() {
connection.console.debug('Clearing state');
compiledFns.clear();
autoDepsDecorations = [];
codeActionEvents = [];
}
documents.listen(connection);
connection.listen();
connection.console.info(`React Analyzer running in node ${process.version}`);

View File

@@ -0,0 +1,35 @@
/**
* 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 {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
import {type Position} from 'vscode-languageserver-textdocument';
import {RequestType} from 'vscode-languageserver/node';
import {type Range, sourceLocationToRange} from '../utils/range';
export type AutoDepsDecorationsLSPEvent = {
useEffectCallExpr: Range;
decorations: Array<Range>;
};
export interface AutoDepsDecorationsParams {
position: Position;
}
export namespace AutoDepsDecorationsRequest {
export const type = new RequestType<
AutoDepsDecorationsParams,
AutoDepsDecorationsLSPEvent,
void
>('react/autodeps_decorations');
}
export function mapCompilerEventToLSPEvent(
event: AutoDepsDecorationsEvent,
): AutoDepsDecorationsLSPEvent {
return {
useEffectCallExpr: sourceLocationToRange(event.fnLoc),
decorations: event.decorations.map(sourceLocationToRange),
};
}

View File

@@ -0,0 +1,42 @@
/**
* 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 * as t from '@babel/types';
import {type Position} from 'vscode-languageserver/node';
export type Range = [Position, Position];
export function isPositionWithinRange(
position: Position,
[start, end]: Range,
): boolean {
return position.line >= start.line && position.line <= end.line;
}
export function isRangeWithinRange(aRange: Range, bRange: Range): boolean {
const startComparison = comparePositions(aRange[0], bRange[0]);
const endComparison = comparePositions(aRange[1], bRange[1]);
return startComparison >= 0 && endComparison <= 0;
}
function comparePositions(a: Position, b: Position): number {
const lineComparison = a.line - b.line;
if (lineComparison === 0) {
return a.character - b.character;
} else {
return lineComparison;
}
}
export function sourceLocationToRange(
loc: t.SourceLocation,
): [Position, Position] {
return [
{line: loc.start.line - 1, character: loc.start.column},
{line: loc.end.line - 1, character: loc.end.column},
];
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "../",
"noEmit": true,
"jsx": "react-jsxdev",
"lib": ["ES2022"],
"target": "ES2022",
"importsNotUsedAsValues": "remove",
},
"exclude": ["node_modules"],
"include": ["server/src/**/*.ts", "client/src/**/*.ts"],
}

View File

@@ -0,0 +1,22 @@
# React MCP Server (experimental)
An experimental MCP Server for React.
## Development
First, add this file if you're using Claude Desktop: `code ~/Library/Application\ Support/Claude/claude_desktop_config.json`. Copy the absolute path from `which node` and from `react/compiler/react-mcp-server/dist/index.js` and paste, for example:
```json
{
"mcpServers": {
"react": {
"command": "/Users/<username>/.asdf/shims/node",
"args": [
"/Users/<username>/code/react/compiler/packages/react-mcp-server/dist/index.js"
]
}
}
}
```
Next, run `yarn workspace react-mcp-server watch` from the `react/compiler` directory and make changes as needed. You will need to restart Claude everytime you want to try your changes.

View File

@@ -0,0 +1,35 @@
{
"name": "react-mcp-server",
"version": "0.0.0",
"description": "React MCP Server (experimental)",
"bin": {
"react-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "rimraf dist && tsup",
"test": "echo 'no tests'",
"dev": "concurrently --kill-others -n build,inspect \"yarn run watch\" \"wait-on dist/index.js && yarn run inspect\"",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
"watch": "yarn build --watch"
},
"dependencies": {
"@babel/core": "^7.26.0",
"@babel/parser": "^7.26",
"@babel/plugin-syntax-typescript": "^7.25.9",
"@modelcontextprotocol/sdk": "^1.9.0",
"algoliasearch": "^5.23.3",
"cheerio": "^1.0.0",
"html-to-text": "^9.0.5",
"prettier": "^3.3.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/html-to-text": "^9.0.4"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react.git",
"directory": "compiler/packages/react-mcp-server"
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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 type * as BabelCore from '@babel/core';
import {parseAsync, transformFromAstAsync} from '@babel/core';
import BabelPluginReactCompiler, {
type PluginOptions,
} from 'babel-plugin-react-compiler/src';
import * as prettier from 'prettier';
export let lastResult: BabelCore.BabelFileResult | null = null;
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
name: string;
fnName: string | null;
value: string;
}
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
| {kind: 'debug'; name: string; fnName: string | null; value: string};
type CompileOptions = {
text: string;
file: string;
options: Partial<PluginOptions> | null;
};
export async function compile({
text,
file,
options,
}: CompileOptions): Promise<BabelCore.BabelFileResult> {
const ast = await parseAsync(text, {
sourceFileName: file,
parserOpts: {
plugins: ['typescript', 'jsx'],
},
sourceType: 'module',
});
if (ast == null) {
throw new Error('Could not parse');
}
const plugins =
options != null
? [[BabelPluginReactCompiler, options]]
: [[BabelPluginReactCompiler]];
const result = await transformFromAstAsync(ast, text, {
filename: file,
highlightCode: false,
retainLines: true,
plugins,
sourceType: 'module',
sourceFileName: file,
});
if (result?.code == null) {
throw new Error(
`Expected BabelPluginReactCompiler to compile successfully, got ${result}`,
);
}
try {
result.code = await prettier.format(result.code, {
semi: false,
parser: 'babel-ts',
});
if (result.code != null) {
lastResult = result;
}
} catch (err) {
// If prettier failed just log, no need to crash
console.error(err);
}
return result;
}

View File

@@ -0,0 +1,365 @@
/**
* 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 {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {z} from 'zod';
import {compile, type PrintedCompilerPipelineValue} from './compiler';
import {
CompilerPipelineValue,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
PluginOptions,
SourceLocation,
} from 'babel-plugin-react-compiler/src';
import * as cheerio from 'cheerio';
import {queryAlgolia} from './utils/algolia';
import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
const server = new McpServer({
name: 'React',
version: '0.0.0',
});
server.tool(
'query-react-dev-docs',
'Search/look up official docs from react.dev',
{
query: z.string(),
},
async ({query}) => {
try {
const pages = await queryAlgolia(query);
if (pages.length === 0) {
return {
content: [{type: 'text' as const, text: `No results`}],
};
}
const content = pages.map(html => {
const $ = cheerio.load(html);
// react.dev should always have at least one <article> with the main content
const article = $('article').html();
if (article != null) {
return {
type: 'text' as const,
text: convert(article),
};
} else {
return {
type: 'text' as const,
// Fallback to converting the whole page to text.
text: convert($.html()),
};
}
});
return {
content,
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
}
},
);
server.tool(
'compile',
'Compile code with React Compiler. Optionally, for debugging provide a pass name like "HIR" to see more information.',
{
text: z.string(),
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
},
async ({text, passName}) => {
const pipelinePasses = new Map<
string,
Array<PrintedCompilerPipelineValue>
>();
const recordPass: (
result: PrintedCompilerPipelineValue,
) => void = result => {
const entry = pipelinePasses.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
pipelinePasses.set(result.name, [result]);
}
};
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
recordPass({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
recordPass({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
recordPass({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
assertExhaustive(result, `Unhandled result ${result}`);
}
}
};
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
const compilerOptions: Partial<PluginOptions> = {
panicThreshold: 'none',
logger: {
debugLogIRs: logIR,
logEvent: (_filename, event): void => {
if (event.kind === 'CompileError') {
const detail = event.detail;
const loc =
detail.loc == null || typeof detail.loc == 'symbol'
? event.fnLoc
: detail.loc;
errors.push({
message: detail.reason,
loc,
});
}
},
},
};
try {
const result = await compile({
text,
file: 'anonymous.tsx',
options: compilerOptions,
});
if (result.code == null) {
return {
isError: true,
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
};
}
const requestedPasses: Array<{type: 'text'; text: string}> = [];
if (passName != null) {
switch (passName) {
case 'All': {
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
if (hir !== undefined) {
for (const pipelineValue of hir) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
if (reactiveFunc !== undefined) {
for (const pipelineValue of reactiveFunc) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
break;
}
case 'HIR': {
// Last pass before HIR -> ReactiveFunction
const requestedPass = pipelinePasses.get(
'PropagateScopeDependenciesHIR',
);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case 'ReactiveFunction': {
// Last pass
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case '@DEBUG': {
for (const [, pipelinePass] of pipelinePasses) {
for (const pass of pipelinePass) {
requestedPasses.push({
type: 'text' as const,
text: `${pass.name}\n\n${pass.value}`,
});
}
}
break;
}
default: {
assertExhaustive(
passName,
`Unhandled passName option: ${passName}`,
);
}
}
const requestedPass = pipelinePasses.get(passName);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
if (pipelineValue.name === passName) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
}
}
if (errors.length > 0) {
return {
content: errors.map(err => {
return {
type: 'text' as const,
text:
err.loc === null || typeof err.loc === 'symbol'
? `React Compiler bailed out:\n\n${err.message}`
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
};
}),
};
}
return {
content: [
{type: 'text' as const, text: result.code},
...requestedPasses,
],
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
}
},
);
server.prompt('review-react-code', () => ({
messages: [
{
role: 'assistant',
content: {
type: 'text',
text: `
## Role
You are a React assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance.
## Follow these guidelines in all code you produce and suggest
Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic.
Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state.
Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables.
Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state.
Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function.
Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context.
Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly.
Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code.
Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side-effects. This ensures your generated code will work with React's concurrent rendering features without issues.
Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests.
Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable.
Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing “flash” states and improving perceived performance.
Server Components - Shift data-heavy logic to the server whenever possible. Break up the more static parts of the app into server components. Break up data fetching into server components. Only client components (denoted by the 'use client' top level directive) need interactivity. By rendering parts of your UI on the server, you reduce the client-side JavaScript needed and avoid sending unnecessary data over the wire. Use Server Components to prefetch and pre-render data, allowing faster initial loads and smaller bundle sizes. This also helps manage or eliminate certain waterfalls by resolving data on the server before streaming the HTML (and partial React tree) to the client.
## Available Tools
- 'docs': Look up documentation from react.dev. Returns text as a string.
- 'compile': Run the user's code through React Compiler. Returns optimized JS/TS code with potential diagnostics.
## Process
1. Analyze the user's code for optimization opportunities:
- Check for React anti-patterns that prevent compiler optimization
- Identify unnecessary manual optimizations (useMemo, useCallback, React.memo) that the compiler can handle
- Look for component structure issues that limit compiler effectiveness
- Think about each suggestion you are making and consult React docs using the docs://{query} resource for best practices
2. Use React Compiler to verify optimization potential:
- Run the code through the compiler and analyze the output
- You can run the compiler multiple times to verify your work
- Check for successful optimization by looking for const $ = _c(n) cache entries, where n is an integer
- Identify bailout messages that indicate where code could be improved
- Compare before/after optimization potential
3. Provide actionable guidance:
- Explain specific code changes with clear reasoning
- Show before/after examples when suggesting changes
- Include compiler results to demonstrate the impact of optimizations
- Only suggest changes that meaningfully improve optimization potential
## Optimization Guidelines
- Avoid mutation of values that are memoized by the compiler
- State updates should be structured to enable granular updates
- Side effects should be isolated and dependencies clearly defined
- The compiler automatically inserts memoization, so manually added useMemo/useCallback/React.memo can often be removed
## Understanding Compiler Output
- Successful optimization adds import { c as _c } from "react/compiler-runtime";
- Successful optimization initializes a constant sized cache with const $ = _c(n), where n is the size of the cache as an integer
- When suggesting changes, try to increase or decrease the number of cached expressions (visible in const $ = _c(n))
- Increase: more memoization coverage
- Decrease: if there are unnecessary dependencies, less dependencies mean less re-rendering
`,
},
},
],
}));
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('React Compiler MCP Server running on stdio');
}
main().catch(error => {
console.error('Fatal error in main():', error);
process.exit(1);
});

View File

@@ -0,0 +1,100 @@
/**
* 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.
*/
// https://github.com/algolia/docsearch/blob/15ebcba606b281aa0dddc4ccb8feb19d396bf79e/packages/docsearch-react/src/types/DocSearchHit.ts
type ContentType =
| 'content'
| 'lvl0'
| 'lvl1'
| 'lvl2'
| 'lvl3'
| 'lvl4'
| 'lvl5'
| 'lvl6';
interface DocSearchHitAttributeHighlightResult {
value: string;
matchLevel: 'full' | 'none' | 'partial';
matchedWords: string[];
fullyHighlighted?: boolean;
}
interface DocSearchHitHighlightResultHierarchy {
lvl0: DocSearchHitAttributeHighlightResult;
lvl1: DocSearchHitAttributeHighlightResult;
lvl2: DocSearchHitAttributeHighlightResult;
lvl3: DocSearchHitAttributeHighlightResult;
lvl4: DocSearchHitAttributeHighlightResult;
lvl5: DocSearchHitAttributeHighlightResult;
lvl6: DocSearchHitAttributeHighlightResult;
}
interface DocSearchHitHighlightResult {
content: DocSearchHitAttributeHighlightResult;
hierarchy: DocSearchHitHighlightResultHierarchy;
hierarchy_camel: DocSearchHitHighlightResultHierarchy[];
}
interface DocSearchHitAttributeSnippetResult {
value: string;
matchLevel: 'full' | 'none' | 'partial';
}
interface DocSearchHitSnippetResult {
content: DocSearchHitAttributeSnippetResult;
hierarchy: DocSearchHitHighlightResultHierarchy;
hierarchy_camel: DocSearchHitHighlightResultHierarchy[];
}
export declare type DocSearchHit = {
objectID: string;
content: string | null;
url: string;
url_without_anchor: string;
type: ContentType;
anchor: string | null;
hierarchy: {
lvl0: string;
lvl1: string;
lvl2: string | null;
lvl3: string | null;
lvl4: string | null;
lvl5: string | null;
lvl6: string | null;
};
_highlightResult: DocSearchHitHighlightResult;
_snippetResult: DocSearchHitSnippetResult;
_rankingInfo?: {
promoted: boolean;
nbTypos: number;
firstMatchedWord: number;
proximityDistance?: number;
geoDistance: number;
geoPrecision?: number;
nbExactWords: number;
words: number;
filters: number;
userScore: number;
matchedGeoLocation?: {
lat: number;
lng: number;
distance: number;
};
};
_distinctSeqID?: number;
__autocomplete_indexName?: string;
__autocomplete_queryID?: string;
__autocomplete_algoliaCredentials?: {
appId: string;
apiKey: string;
};
__autocomplete_id?: number;
};
export type InternalDocSearchHit = DocSearchHit & {
__docsearch_parent: InternalDocSearchHit | null;
};

View File

@@ -0,0 +1,119 @@
/**
* 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 type {DocSearchHit, InternalDocSearchHit} from '../types/algolia';
import {liteClient, type Hit, type SearchResponse} from 'algoliasearch/lite';
// https://github.com/reactjs/react.dev/blob/55986965fbf69c2584040039c9586a01bd54eba7/src/siteConfig.js#L15-L19
const ALGOLIA_CONFIG = {
appId: '1FCF9AYYAT',
apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
indexName: 'beta-react',
};
export const ALGOLIA_CLIENT = liteClient(
ALGOLIA_CONFIG.appId,
ALGOLIA_CONFIG.apiKey,
);
export function printHierarchy(
hit: DocSearchHit | InternalDocSearchHit,
): string {
let val = `${hit.hierarchy.lvl0} > ${hit.hierarchy.lvl1}`;
if (hit.hierarchy.lvl2 != null) {
val = val.concat(` > ${hit.hierarchy.lvl2}`);
}
if (hit.hierarchy.lvl3 != null) {
val = val.concat(` > ${hit.hierarchy.lvl3}`);
}
if (hit.hierarchy.lvl4 != null) {
val = val.concat(` > ${hit.hierarchy.lvl4}`);
}
if (hit.hierarchy.lvl5 != null) {
val = val.concat(` > ${hit.hierarchy.lvl5}`);
}
if (hit.hierarchy.lvl6 != null) {
val = val.concat(` > ${hit.hierarchy.lvl6}`);
}
return val;
}
export async function queryAlgolia(
message: string | Array<string>,
): Promise<Array<string>> {
const {results} = await ALGOLIA_CLIENT.search<DocSearchHit>({
requests: [
{
query: Array.isArray(message) ? message.join('\n') : message,
indexName: ALGOLIA_CONFIG.indexName,
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'url',
],
attributesToSnippet: [
`hierarchy.lvl1:10`,
`hierarchy.lvl2:10`,
`hierarchy.lvl3:10`,
`hierarchy.lvl4:10`,
`hierarchy.lvl5:10`,
`hierarchy.lvl6:10`,
`content:10`,
],
snippetEllipsisText: '…',
hitsPerPage: 30,
attributesToHighlight: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
],
},
],
});
const firstResult = results[0] as SearchResponse<DocSearchHit>;
const {hits} = firstResult;
const deduped = new Map();
for (const hit of hits) {
// drop hashes to dedupe properly
const u = new URL(hit.url);
if (deduped.has(u.pathname)) {
continue;
}
deduped.set(u.pathname, hit);
}
const pages: Array<string | null> = await Promise.all(
Array.from(deduped.values()).map(hit => {
return fetch(hit.url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
},
}).then(res => {
if (res.ok === true) {
return res.text();
} else {
console.error(
`Could not fetch docs: ${res.status} ${res.statusText}`,
);
return null;
}
});
}),
);
return pages.filter(page => page !== null);
}

View File

@@ -0,0 +1,13 @@
/**
* 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.
*/
/**
* Trigger an exhaustiveness check in TypeScript and throw at runtime.
*/
export default function assertExhaustive(_: never, errorMsg: string): never {
throw new Error(errorMsg);
}

View File

@@ -0,0 +1,5 @@
TODO
- [ ] If code doesnt compile, read diagnostics and try again
- [ ] Provide detailed examples in assistant prompt (use another LLM to generate good prompts, iterate from there)
- [ ] Provide more tools for working with HIR/AST (eg so we can prompt it to try and optimize code via HIR, which it can then translate back into user code changes)

View File

@@ -0,0 +1,22 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "../",
"noEmit": true,
"jsx": "react-jsxdev",
"lib": ["ES2022"],
// weaken strictness from preset
"importsNotUsedAsValues": "remove",
"noUncheckedIndexedAccess": false,
"noUnusedParameters": false,
"useUnknownInCatchVariables": false,
"target": "ES2022",
// ideally turn off only during dev, or on a per-file basis
"noUnusedLocals": false,
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts"],
}

View File

@@ -0,0 +1,37 @@
/**
* 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 {defineConfig} from 'tsup';
export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [],
splitting: false,
sourcemap: false,
dts: false,
bundle: true,
format: 'cjs',
platform: 'node',
target: 'es2022',
banner: {
js: `#!/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.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/`,
},
});

View File

@@ -306,6 +306,7 @@ export type TransformResult = {
original: string;
forget: string;
} | null;
sourceMap: BabelCore.BabelFileResult['map'];
};
export async function transformFixtureInput(
@@ -331,6 +332,9 @@ export async function transformFixtureInput(
// with `cwd`, which is different across machines
const virtualFilepath = '/' + filename;
// Check if we should emit source maps in the test fixture
const includeSourceMaps = firstLine.includes('@sourceMaps');
const presets =
language === 'typescript'
? TypescriptEvaluatorPresets
@@ -357,6 +361,7 @@ export async function transformFixtureInput(
'babel-plugin-idx',
],
sourceType: 'module',
sourceMaps: includeSourceMaps,
ast: includeEvaluator,
cloneInputAst: includeEvaluator,
configFile: false,
@@ -447,6 +452,7 @@ export async function transformFixtureInput(
forgetOutput,
logs: formattedLogs,
evaluatorCode,
sourceMap: includeSourceMaps ? forgetResult.map : null,
},
};
}

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {BabelFileResult} from '@babel/core';
import chalk from 'chalk';
import fs from 'fs';
import invariant from 'invariant';
@@ -24,6 +25,7 @@ export function writeOutputToString(
evaluatorOutput: string | null,
logs: string | null,
errorMessage: string | null,
sourceMap: BabelFileResult['map'] | null,
) {
// leading newline intentional
let result = `
@@ -42,6 +44,14 @@ ${wrapWithTripleBackticks(compilerOutput, 'javascript')}
result += '\n';
}
if (sourceMap != null) {
result += `
## Source Map
${wrapWithTripleBackticks(JSON.stringify(sourceMap, null, 2))}
`;
}
if (logs != null) {
result += `
## Logs

View File

@@ -245,6 +245,7 @@ export async function transformFixture(
sproutOutput,
compileResult?.logs ?? null,
error,
compileResult?.sourceMap ?? null,
);
return {

View File

@@ -51,6 +51,9 @@ if (hasErrors) {
}
function processFile(file) {
if (fs.lstatSync(file).isDirectory()) {
return;
}
let source = fs.readFileSync(file, 'utf8');
if (source.indexOf(META_COPYRIGHT_COMMENT_BLOCK) === 0) {

View File

@@ -1,4 +1,10 @@
#!/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 prompt = require('prompt-promise');

View File

@@ -1,4 +1,10 @@
#!/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.
*/
'use strict';
@@ -62,9 +68,15 @@ async function main() {
.option('tag', {
description: 'Tag to publish to npm',
type: 'choices',
choices: ['experimental', 'beta'],
choices: ['experimental', 'beta', 'rc'],
default: 'experimental',
})
.option('tag-version', {
description:
'Optional tag version to append to tag name, eg `1` becomes 0.0.0-rc.1',
type: 'number',
default: null,
})
.option('version-name', {
description: 'Version name',
type: 'string',
@@ -133,7 +145,13 @@ async function main() {
files: {exclude: ['.DS_Store']},
});
const truncatedHash = hash.slice(0, 7);
const newVersion = `${argv.versionName}-${argv.tag}-${truncatedHash}-${dateString}`;
let newVersion =
argv.tagVersion == null || argv.tagVersion === ''
? `${argv.versionName}-${argv.tag}`
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
if (argv.tag === 'experimental' || argv.tag === 'beta') {
newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
}
for (const pkgName of pkgNames) {
const pkgDir = path.resolve(__dirname, `../../packages/${pkgName}`);

View File

@@ -1,3 +1,10 @@
/**
* 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 ora = require('ora');
const {execHelper} = require('./utils');

View File

@@ -1,3 +1,10 @@
/**
* 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 PUBLISHABLE_PACKAGES = [
'babel-plugin-react-compiler',
'eslint-plugin-react-compiler',

View File

@@ -1,3 +1,10 @@
/**
* 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 cp = require('child_process');
const util = require('util');

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,7 @@
"webpack-manifest-plugin": "^4.0.2"
},
"devDependencies": {
"@playwright/test": "^1.49.1"
"@playwright/test": "^1.51.1"
},
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",

View File

@@ -2748,12 +2748,12 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.49.1":
version "1.49.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827"
integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==
"@playwright/test@^1.51.1":
version "1.51.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
dependencies:
playwright "1.49.1"
playwright "1.51.1"
"@pmmmwh/react-refresh-webpack-plugin@0.5.15":
version "0.5.15"
@@ -7284,17 +7284,17 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
playwright-core@1.49.1:
version "1.49.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015"
integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==
playwright-core@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
playwright@1.49.1:
version "1.49.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c"
integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==
playwright@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
dependencies:
playwright-core "1.49.1"
playwright-core "1.51.1"
optionalDependencies:
fsevents "2.3.2"

View File

@@ -1,5 +1,6 @@
import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {Writable} from 'stream';
import App from '../src/components/App';
@@ -14,11 +15,41 @@ if (process.env.NODE_ENV === 'development') {
assets = require('../build/asset-manifest.json');
}
class ThrottledWritable extends Writable {
constructor(destination) {
super();
this.destination = destination;
this.delay = 150;
}
_write(chunk, encoding, callback) {
let o = 0;
const write = () => {
this.destination.write(chunk.slice(o, o + 100), encoding, x => {
o += 100;
if (o < chunk.length) {
setTimeout(write, this.delay);
} else {
callback(x);
}
});
};
setTimeout(write, this.delay);
}
_final(callback) {
setTimeout(() => {
this.destination.end(callback);
}, this.delay);
}
}
export default function render(url, res) {
res.socket.on('error', error => {
// Log fatal errors
console.error('Fatal', error);
});
console.log('hello');
let didError = false;
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
bootstrapScripts: [assets['main.js']],
@@ -26,7 +57,10 @@ export default function render(url, res) {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
// To test the actual chunks taking time to load over the network, we throttle
// the stream a bit.
const throttledResponse = new ThrottledWritable(res);
pipe(throttledResponse);
},
onShellError(x) {
// Something errored before we could complete the shell so we emit an alternative shell.

View File

@@ -4,6 +4,8 @@ import Theme, {ThemeToggleButton} from './Theme';
import './Chrome.css';
import LargeContent from './LargeContent';
export default class Chrome extends Component {
state = {theme: 'light'};
render() {
@@ -25,7 +27,6 @@ export default class Chrome extends Component {
/>
<Suspense fallback="Loading...">
<Theme.Provider value={this.state.theme}>
{this.props.children}
<div>
<ThemeToggleButton
onChange={theme => {
@@ -35,8 +36,14 @@ export default class Chrome extends Component {
}}
/>
</div>
{this.props.children}
</Theme.Provider>
</Suspense>
<p>This should appear in the first paint.</p>
<Suspense fallback="Loading...">
<p>This content should not block paint.</p>
<LargeContent />
</Suspense>
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,

View File

@@ -0,0 +1,243 @@
import React, {Fragment} from 'react';
export default function LargeContent() {
return (
<Fragment>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.
Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,
aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras
facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet
tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante
sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius
aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.
Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.
Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed
ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum
fermentum quam id aliquam. Donec porta risus vitae pretium posuere.
Fusce facilisis eros in lacus tincidunt congue.
</p>
<p>
Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Phasellus dolor ante, iaculis vel nisl vitae,
ornare ornare orci. Praesent sit amet lobortis sapien. Suspendisse
pharetra posuere libero ut dapibus. Donec condimentum ante urna. Aliquam
laoreet tincidunt lacus, sed interdum tortor dapibus elementum. Nam sed
faucibus lorem. Suspendisse finibus, velit sed molestie finibus, risus
purus mollis ante, sit amet aliquet sapien nulla ut nibh. In eget ligula
metus. Duis in purus mattis, blandit magna nec, dictum nunc.
</p>
<p>
Sed convallis magna id tortor blandit dictum. Suspendisse in porttitor
neque. Integer quis metus consequat, rutrum est sit amet, finibus justo.
In hac habitasse platea dictumst. Nullam sagittis, risus sed vehicula
porta, sapien elit ultrices nibh, vel luctus odio tortor et ante. Sed
porta enim in hendrerit tristique. Pellentesque id feugiat libero, sit
amet tempor enim. Proin gravida nisl justo, vel ornare dolor bibendum
ac. Mauris scelerisque mattis facilisis. Praesent sodales augue mollis
orci vulputate aliquet. Mauris molestie luctus neque, sed congue elit
congue ut. Cras quis tortor augue. In auctor nulla vel turpis dapibus
egestas. Phasellus consequat rhoncus nisi sed dignissim. Quisque varius
justo non ex lobortis finibus cursus nec justo. Nulla erat neque,
commodo et sem convallis, tristique faucibus odio.
</p>
<p>
Ut condimentum volutpat sem, id accumsan augue placerat vel. Donec ac
efficitur turpis. Suspendisse pretium odio euismod sapien bibendum, sed
tempus est condimentum. Etiam nisl magna, consequat at ullamcorper at,
sollicitudin eu eros. In mattis ligula arcu. Sed eu consectetur turpis,
id molestie ligula. Vestibulum et venenatis enim. Donec condimentum
vitae nisi et placerat. Sed fringilla vehicula egestas. Proin
consectetur, nibh non ornare scelerisque, diam lorem cursus lectus, ut
mattis mauris purus id mi. Curabitur non ligula sit amet augue molestie
vulputate. Donec maximus magna at volutpat aliquet. Pellentesque
dignissim nulla eget odio eleifend tincidunt. Etiam diam lorem, ornare
vel scelerisque vel, iaculis id risus. Donec aliquet aliquam felis, ac
vehicula lacus suscipit vitae. Morbi eu ligula elit.
</p>
<p>
Praesent pellentesque, libero ut faucibus tempor, purus elit consequat
metus, in ornare nulla lectus at erat. Duis quis blandit turpis. Fusce
at ligula rutrum metus molestie tempor sit amet eu justo. Maecenas
tincidunt nisl nunc. Morbi ac metus tempor, pretium arcu vel, dapibus
velit. Nulla convallis ligula at porta mollis. Duis magna ante, mollis
eget nibh in, congue tempor dolor. Sed tincidunt sagittis arcu, in
ultricies neque tempor non. Suspendisse eget nunc neque. Nulla sit amet
odio volutpat, maximus purus id, dictum metus. Integer consequat, orci
nec ullamcorper porta, mauris libero vestibulum ipsum, nec tempor tellus
enim non nunc. Quisque nisl risus, dapibus sit amet purus nec, aliquam
finibus metus. Nullam condimentum urna viverra finibus cursus. Proin et
sollicitudin tellus, porta fermentum felis. Maecenas ac turpis sed dui
condimentum interdum sed sed erat. Mauris ut dignissim erat.
</p>
<p>
Proin varius porta dui, id fringilla elit lobortis eget. Integer at
metus elementum, efficitur eros id, euismod est. Morbi vestibulum nibh
ac leo luctus sagittis. Praesent rhoncus, risus sit amet mattis dictum,
diam sapien tempor neque, vel dignissim nulla neque eget ex. Nam
sollicitudin metus quis ullamcorper dapibus. Nam tristique euismod
efficitur. Pellentesque rhoncus vel sem eget lacinia. Pellentesque
volutpat velit ac dignissim luctus. Vivamus euismod tortor at ligula
mattis porta. Vestibulum ante ipsum primis in faucibus orci luctus et
ultrices posuere cubilia curae;
</p>
<p>
Proin blandit vulputate efficitur. Pellentesque sit amet porta odio.
Nunc pulvinar varius rhoncus. Mauris fermentum leo a imperdiet pretium.
Mauris scelerisque justo vel ante egestas, eget tempus neque malesuada.
Sed dictum ex vel justo dignissim, aliquam commodo diam rutrum. Integer
dignissim est ullamcorper augue laoreet consectetur id at diam. Vivamus
molestie blandit urna, eget pulvinar augue dictum vestibulum. Duis
maximus bibendum mauris, ut ultricies elit rhoncus eu. Praesent gravida
placerat mauris. Praesent tempor ipsum at nibh rhoncus sagittis. Duis
non sem turpis. Quisque et metus leo. Sed eu purus lorem. Pellentesque
dictum metus sed leo viverra interdum. Maecenas vel tincidunt mi.
</p>
<p>
Praesent consequat dapibus pellentesque. Fusce at enim id mauris laoreet
commodo. Nullam ut mauris euismod, rhoncus tellus vel, facilisis diam.
Aenean porta faucibus augue, a iaculis massa iaculis in. Praesent vel
metus purus. Etiam quis augue eget orci lobortis eleifend ac ut lorem.
Aenean non orci quis nisi molestie maximus. Mauris interdum, eros et
aliquam aliquam, lectus diam pharetra velit, in condimentum odio eros
non quam. Praesent bibendum pretium turpis vitae tristique. Mauris
convallis, massa ut fermentum fermentum, libero orci tempus ipsum,
malesuada ultrices metus sapien placerat lectus. Ut fringilla arcu nec
lorem ultrices mattis. Etiam id tortor feugiat magna gravida gravida.
Morbi aliquam, mi ac pellentesque mattis, erat ex venenatis erat, a
vestibulum eros turpis quis metus. Pellentesque tempus justo in ligula
ultricies porta. Phasellus congue felis sit amet dolor tristique
finibus. Nunc eget eros non est ultricies vestibulum.
</p>
<p>
Donec efficitur ligula quis odio tincidunt tristique. Duis urna dolor,
hendrerit quis enim at, accumsan auctor turpis. Vivamus ante lorem,
maximus vitae suscipit ut, congue eget velit. Maecenas sed ligula erat.
Aliquam mollis purus at nisi porta suscipit in ut magna. Vivamus a
turpis nec tellus egestas suscipit nec ornare nisi. Donec vestibulum
libero quis ex suscipit, sit amet luctus leo gravida.
</p>
<p>
Praesent pharetra dolor elit, sed volutpat lorem rhoncus non. Etiam a
neque ut velit dignissim sodales. Vestibulum neque risus, condimentum
nec consectetur vitae, ultricies ut sapien. Integer iaculis at urna sit
amet malesuada. Integer tincidunt, felis ac vulputate semper, velit leo
facilisis lorem, quis aliquet leo dui id lorem. Morbi non quam quis nisl
sagittis consequat nec vitae libero. Nunc molestie pretium libero, eu
eleifend nibh feugiat sed. Ut in bibendum diam, sit amet vehicula risus.
Nam ornare ac nisi ac euismod. Nullam id egestas nulla. Etiam porta
commodo ante sit amet pellentesque. Suspendisse eleifend purus in urna
euismod auctor non vel nisi. Suspendisse rutrum est nunc, sit amet
lacinia lacus dictum eget. Pellentesque habitant morbi tristique
senectus et netus et malesuada fames ac turpis egestas. Morbi a blandit
diam.
</p>
<p>
Donec eget efficitur sapien. Suspendisse diam lacus, varius eu interdum
et, congue ac justo. Proin ipsum odio, suscipit elementum mauris sed,
porttitor congue est. Cras dapibus dictum ante, vitae gravida elit
venenatis sed. Sed massa sem, posuere ut enim sit amet, vestibulum
condimentum nibh. Pellentesque pulvinar sodales lacinia. Proin id
pretium sapien, non convallis nulla. In mollis tincidunt sem et
porttitor.
</p>
<p>
Integer at sollicitudin sem. Suspendisse sed semper orci. Nulla at nibh
nec risus suscipit posuere egestas vitae enim. Nullam mauris justo,
mattis vel laoreet non, finibus nec nisl. Cras iaculis ultrices nibh,
non commodo eros aliquam non. Sed vitae mollis dui, at maximus metus. Ut
vestibulum, enim ut lobortis vulputate, lorem urna congue elit, non
dictum odio lorem eget velit. Morbi eleifend id ligula vitae vulputate.
Suspendisse ac laoreet justo. Proin eu mattis diam.
</p>
<p>
Nunc in ex quis enim ullamcorper scelerisque eget ac eros. Class aptent
taciti sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos. Aliquam turpis dui, egestas a rhoncus non, fermentum in
tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
posuere cubilia curae; Aenean non risus arcu. Nam ultricies lacinia
volutpat. Class aptent taciti sociosqu ad litora torquent per conubia
nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur
adipiscing elit.
</p>
<p>
Aliquam a felis leo. Proin lorem ipsum, congue eu cursus in, rhoncus ut
libero. Vestibulum sit amet consequat nunc. Ut eleifend lobortis lacus,
vel molestie metus viverra eget. Nullam suscipit eu magna scelerisque
suscipit. Donec dictum in diam nec lacinia. Mauris pellentesque ex ut
purus facilisis, eget placerat turpis semper. Sed dapibus lorem ante, et
malesuada dui eleifend ac. Sed diam felis, semper ac nulla vel, posuere
ultricies ante.
</p>
<p>
Nunc elementum odio sapien, sit amet vulputate lorem varius at. Fusce
non sapien vitae lorem aliquam pretium sit amet congue dolor. Nunc quis
tortor luctus, pretium ex a, tincidunt urna. Aliquam fermentum massa a
erat pharetra varius. Curabitur at auctor dui. Sed posuere pellentesque
massa, vel bibendum urna dictum non. Fusce eget rhoncus urna. Maecenas
sed lectus tellus. Pellentesque convallis dapibus nisl vitae venenatis.
Quisque ornare a dolor ac pharetra. Nam cursus, mi a lacinia accumsan,
felis erat fringilla magna, ac mattis nunc ante a orci.
</p>
<p>
Nunc vel tortor euismod, commodo tortor non, aliquam nisi. Maecenas
tempus mollis velit non suscipit. Mauris sit amet dolor sed ex fringilla
varius. Suspendisse vel cursus risus. Vivamus pharetra massa nec dolor
aliquam feugiat. Fusce finibus enim commodo, scelerisque ante eu,
laoreet ex. Curabitur placerat magna quis imperdiet lacinia. Etiam
lectus mauris, porttitor ac lacinia sed, posuere eget lacus. Mauris
vulputate mattis imperdiet. Nunc id aliquet libero, vitae hendrerit
purus. Praesent vestibulum urna ac egestas tempor. In molestie, nunc sit
amet sagittis dapibus, ligula enim fermentum mi, lacinia molestie eros
dui in tortor. Mauris fermentum pulvinar faucibus. Curabitur laoreet
eleifend purus, non tincidunt tortor gravida nec. Nam eu lectus congue,
commodo libero et, porttitor est. Nullam tincidunt, nisi eu congue
congue, magna justo commodo massa, nec efficitur dui lectus non sem.
</p>
<p>
Nullam vehicula, ipsum quis lacinia tristique, elit nulla dignissim
augue, at pulvinar metus justo ac magna. Nullam nec nunc ac sapien
mollis cursus eu ac enim. Pellentesque a pharetra erat. Ut tempor magna
nisi, accumsan blandit lectus volutpat nec. Vivamus vel lorem nec eros
blandit dictum eget ac diam. Nulla nec turpis dolor. Morbi eu euismod
libero. Nam ut tortor at arcu porta tincidunt. In gravida ligula
fringilla ornare imperdiet. Nulla scelerisque ante erat, efficitur
dictum metus ullamcorper vel. Nam ac purus metus. Maecenas eget tempus
nulla. Ut magna lorem, efficitur ut ex a, semper aliquam magna. Praesent
lobortis, velit ac posuere mattis, justo est accumsan turpis, id
sagittis felis mi in lacus.
</p>
<p>
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
consequat sem quis sem consequat, non aliquam est placerat. Cras
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
Proin nec odio in nulla vestibulum.
</p>
<p>
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
consequat sem quis sem consequat, non aliquam est placerat. Cras
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
Proin nec odio in nulla vestibulum.
</p>
<p>
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
consequat sem quis sem consequat, non aliquam est placerat. Cras
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
Proin nec odio in nulla vestibulum.
</p>
</Fragment>
);
}

View File

@@ -146,7 +146,7 @@
"publish-prereleases": "echo 'This command has been deprecated. Please refer to https://github.com/facebook/react/tree/main/scripts/release#trigger-an-automated-prerelease'",
"download-build": "node ./scripts/release/download-experimental-build.js",
"download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
"download-build-in-codesandbox-ci": "yarn build --type=node react/index react-dom/index react-dom/client react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime",
"download-build-in-codesandbox-ci": "yarn build --type=node react/index react.react-server react-dom/index react-dom/client react-dom/src/server react-dom/test-utils react-dom.react-server scheduler/index react/jsx-runtime react/jsx-dev-runtime react-server-dom-webpack",
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js",
"flags": "node ./scripts/flags/flags.js"

View File

@@ -1,6 +1,6 @@
/* global chrome */
import {normalizeUrl} from 'react-devtools-shared/src/utils';
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
let debugIDCounter = 0;
@@ -117,7 +117,7 @@ async function fetchFileWithCaching(url: string): Promise<string> {
chrome.devtools.inspectedWindow.getResources(r => resolve(r)),
);
const normalizedReferenceURL = normalizeUrl(url);
const normalizedReferenceURL = normalizeUrlIfValid(url);
const resource = resources.find(r => r.url === normalizedReferenceURL);
if (resource != null) {

View File

@@ -16,6 +16,7 @@ import {
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
} from 'react-devtools-shared/src/constants';
import {logEvent} from 'react-devtools-shared/src/Logger';
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
import {
setBrowserSelectionFromReact,
@@ -128,7 +129,11 @@ function createBridgeAndStore() {
: source;
// We use 1-based line and column, Chrome expects them 0-based.
chrome.devtools.panels.openResource(sourceURL, line - 1, column - 1);
chrome.devtools.panels.openResource(
normalizeUrlIfValid(sourceURL),
line - 1,
column - 1,
);
};
// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.

View File

@@ -33,7 +33,7 @@
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@playwright/test": "^1.16.3",
"@playwright/test": "^1.51.1",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.4",

View File

@@ -815,6 +815,130 @@ describe('InspectedElement', () => {
`);
});
it('should support Thenables in React 19', async () => {
const Example = () => null;
class SubclassedPromise extends Promise {}
const plainThenable = {then() {}};
const subclassedPromise = new SubclassedPromise(() => {});
const unusedPromise = Promise.resolve();
const usedFulfilledPromise = Promise.resolve();
const usedFulfilledRichPromise = Promise.resolve({
some: {
deeply: {
nested: {
object: {
string: 'test',
fn: () => {},
},
},
},
},
});
const usedPendingPromise = new Promise(resolve => {});
const usedRejectedPromise = Promise.reject(
new Error('test-error-do-not-surface'),
);
function Use({value}) {
React.use(value);
}
await utils.actAsync(() =>
render(
<>
<Example
plainThenable={plainThenable}
subclassedPromise={subclassedPromise}
unusedPromise={unusedPromise}
usedFulfilledPromise={usedFulfilledPromise}
usedFulfilledRichPromise={usedFulfilledRichPromise}
usedPendingPromise={usedPendingPromise}
usedRejectedPromise={usedRejectedPromise}
/>
<React.Suspense>
<Use value={usedPendingPromise} />
</React.Suspense>
<React.Suspense>
<Use value={usedFulfilledPromise} />
</React.Suspense>
<React.Suspense>
<Use value={usedFulfilledRichPromise} />
</React.Suspense>
<ErrorBoundary>
<React.Suspense>
<Use value={usedRejectedPromise} />
</React.Suspense>
</ErrorBoundary>
</>,
),
);
const inspectedElement = await inspectElementAtIndex(0);
expect(inspectedElement.props).toMatchInlineSnapshot(`
{
"plainThenable": Dehydrated {
"preview_short": Thenable,
"preview_long": Thenable,
},
"subclassedPromise": Dehydrated {
"preview_short": SubclassedPromise,
"preview_long": SubclassedPromise,
},
"unusedPromise": Dehydrated {
"preview_short": Promise,
"preview_long": Promise,
},
"usedFulfilledPromise": {
"value": undefined,
},
"usedFulfilledRichPromise": {
"value": Dehydrated {
"preview_short": {…},
"preview_long": {some: {…}},
},
},
"usedPendingPromise": Dehydrated {
"preview_short": pending Promise,
"preview_long": pending Promise,
},
"usedRejectedPromise": {
"reason": Dehydrated {
"preview_short": Error: test-error-do-not-surface,
"preview_long": Error: test-error-do-not-surface,
},
},
}
`);
});
it('should support Promises in React 18', async () => {
const Example = () => null;
const unusedPromise = Promise.resolve();
await utils.actAsync(() =>
render(
<>
<Example unusedPromise={unusedPromise} />
</>,
),
);
const inspectedElement = await inspectElementAtIndex(0);
expect(inspectedElement.props).toMatchInlineSnapshot(`
{
"unusedPromise": Dehydrated {
"preview_short": Promise,
"preview_long": Promise,
},
}
`);
});
it('should not consume iterables while inspecting', async () => {
const Example = () => null;

View File

@@ -27,6 +27,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-background-selected': '#0088fa',
'--color-button-background': '#ffffff',
'--color-button-background-focus': '#ededed',
'--color-button-background-hover': 'rgba(0, 0, 0, 0.2)',
'--color-button': '#5f6673',
'--color-button-disabled': '#cfd1d5',
'--color-button-active': '#0088fa',
@@ -174,6 +175,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-background-selected': '#178fb9',
'--color-button-background': '#282c34',
'--color-button-background-focus': '#3d424a',
'--color-button-background-hover': 'rgba(255, 255, 255, 0.2)',
'--color-button': '#afb3b9',
'--color-button-active': '#61dafb',
'--color-button-disabled': '#4f5766',

View File

@@ -19,7 +19,6 @@ import {
} from 'react-devtools-shared/src/storage';
import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
import InspectedElement from './InspectedElement';
import {InspectedElementContextController} from './InspectedElementContext';
import {ModalDialog} from '../ModalDialog';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import {NativeStyleContextController} from './NativeStyleEditor/context';
@@ -162,9 +161,7 @@ function Components(_: {}) {
<div className={styles.InspectedElementWrapper}>
<NativeStyleContextController>
<InspectedElementErrorBoundary>
<InspectedElementContextController>
<InspectedElement />
</InspectedElementContextController>
<InspectedElement />
</InspectedElementErrorBoundary>
</NativeStyleContextController>
</div>

View File

@@ -28,6 +28,7 @@ import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
import {InspectedElementContextController} from './Components/InspectedElementContext';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext';
@@ -276,43 +277,47 @@ export default function DevTools({
<TreeContextController>
<ProfilerContextController>
<TimelineContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
<InspectedElementContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={
componentsPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
</div>
</ThemeProvider>
</ThemeProvider>
</InspectedElementContextController>
</TimelineContextController>
</ProfilerContextController>
</TreeContextController>

View File

@@ -0,0 +1,59 @@
.LoadHookNamesToggle,
.ToggleError {
padding: 2px;
background: none;
border: none;
cursor: pointer;
position: relative;
bottom: -0.2em;
margin-block: -1em;
}
.ToggleError {
color: var(--color-error-text);
}
.Hook {
list-style-type: none;
margin: 0;
padding-left: 0.5rem;
line-height: 1.125rem;
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
}
.Hook .Hook {
padding-left: 1rem;
}
.Name {
color: var(--color-dim);
flex: 0 0 auto;
cursor: default;
}
.PrimitiveHookName {
color: var(--color-text);
flex: 0 0 auto;
cursor: default;
}
.Name:after {
color: var(--color-text);
content: ': ';
margin-right: 0.5rem;
}
.PrimitiveHookNumber {
background-color: var(--color-primitive-hook-badge-background);
color: var(--color-primitive-hook-badge-text);
font-size: var(--font-size-monospace-small);
margin-right: 0.25rem;
border-radius: 0.125rem;
padding: 0.125rem 0.25rem;
}
.HookName {
color: var(--color-component-name);
}

View File

@@ -0,0 +1,207 @@
/**
* 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.
*
* @flow
*/
import * as React from 'react';
import {
useContext,
useMemo,
useCallback,
memo,
useState,
useEffect,
} from 'react';
import styles from './HookChangeSummary.css';
import ButtonIcon from '../ButtonIcon';
import {InspectedElementContext} from '../Components/InspectedElementContext';
import {StoreContext} from '../context';
import {
getAlreadyLoadedHookNames,
getHookSourceLocationKey,
} from 'react-devtools-shared/src/hookNamesCache';
import Toggle from '../Toggle';
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
import type {ChangeDescription} from './types';
// $FlowFixMe: Flow doesn't know about Intl.ListFormat
const hookListFormatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
});
type HookProps = {
hook: HooksNode,
hookNames: Map<string, string> | null,
};
const Hook: React.AbstractComponent<HookProps> = memo(({hook, hookNames}) => {
const hookSource = hook.hookSource;
const hookName = useMemo(() => {
if (!hookSource || !hookNames) return null;
const key = getHookSourceLocationKey(hookSource);
return hookNames.get(key) || null;
}, [hookSource, hookNames]);
return (
<ul className={styles.Hook}>
<li>
{hook.id !== null && (
<span className={styles.PrimitiveHookNumber}>
{String(hook.id + 1)}
</span>
)}
<span
className={hook.id !== null ? styles.PrimitiveHookName : styles.Name}>
{hook.name}
{hookName && <span className={styles.HookName}>({hookName})</span>}
</span>
{hook.subHooks?.map((subHook, index) => (
<Hook key={hook.id} hook={subHook} hookNames={hookNames} />
))}
</li>
</ul>
);
});
const shouldKeepHook = (
hook: HooksNode,
hooksArray: Array<number>,
): boolean => {
if (hook.id !== null && hooksArray.includes(hook.id)) {
return true;
}
const subHooks = hook.subHooks;
if (subHooks == null) {
return false;
}
return subHooks.some(subHook => shouldKeepHook(subHook, hooksArray));
};
const filterHooks = (
hook: HooksNode,
hooksArray: Array<number>,
): HooksNode | null => {
if (!shouldKeepHook(hook, hooksArray)) {
return null;
}
const subHooks = hook.subHooks;
if (subHooks == null) {
return hook;
}
const filteredSubHooks = subHooks
.map(subHook => filterHooks(subHook, hooksArray))
.filter(Boolean);
return filteredSubHooks.length > 0
? {...hook, subHooks: filteredSubHooks}
: hook;
};
type Props = {|
fiberID: number,
hooks: $PropertyType<ChangeDescription, 'hooks'>,
state: $PropertyType<ChangeDescription, 'state'>,
displayMode?: 'detailed' | 'compact',
|};
const HookChangeSummary: React.AbstractComponent<Props> = memo(
({hooks, fiberID, state, displayMode = 'detailed'}: Props) => {
const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext(
InspectedElementContext,
);
const store = useContext(StoreContext);
const [parseHookNamesOptimistic, setParseHookNamesOptimistic] =
useState<boolean>(parseHookNames);
useEffect(() => {
setParseHookNamesOptimistic(parseHookNames);
}, [inspectedElement?.id, parseHookNames]);
const handleOnChange = useCallback(() => {
setParseHookNamesOptimistic(!parseHookNames);
toggleParseHookNames();
}, [toggleParseHookNames, parseHookNames]);
const element = fiberID !== null ? store.getElementByID(fiberID) : null;
const hookNames =
element != null ? getAlreadyLoadedHookNames(element) : null;
const filteredHooks = useMemo(() => {
if (!hooks || !inspectedElement?.hooks) return null;
return inspectedElement.hooks
.map(hook => filterHooks(hook, hooks))
.filter(Boolean);
}, [inspectedElement?.hooks, hooks]);
const hookParsingFailed = parseHookNames && hookNames === null;
if (!hooks?.length) {
return <span>No hooks changed</span>;
}
if (
inspectedElement?.id !== element?.id ||
filteredHooks?.length !== hooks.length ||
displayMode === 'compact'
) {
const hookIds = hooks.map(hookId => String(hookId + 1));
const hookWord = hookIds.length === 1 ? '• Hook' : '• Hooks';
return (
<span>
{hookWord} {hookListFormatter.format(hookIds)} changed
</span>
);
}
let toggleTitle: string;
if (hookParsingFailed) {
toggleTitle = 'Hook parsing failed';
} else if (parseHookNamesOptimistic) {
toggleTitle = 'Parsing hook names ...';
} else {
toggleTitle = 'Parse hook names (may be slow)';
}
if (filteredHooks == null) {
return null;
}
return (
<div>
{filteredHooks.length > 1 ? '• Hooks changed:' : '• Hook changed:'}
{(!parseHookNames || hookParsingFailed) && (
<Toggle
className={
hookParsingFailed
? styles.ToggleError
: styles.LoadHookNamesToggle
}
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
onChange={handleOnChange}
title={toggleTitle}>
<ButtonIcon type="parse-hook-names" />
</Toggle>
)}
{filteredHooks.map(hook => (
<Hook
key={`${inspectedElement?.id ?? 'unknown'}-${hook.id}`}
hook={hook}
hookNames={hookNames}
/>
))}
</div>
);
},
);
export default HookChangeSummary;

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