Compare commits

..

91 Commits

Author SHA1 Message Date
Dennis Moradkhani 4b877b6c66
Updated copyright text to Copyright (c) Meta Platforms, Inc. and its … (#26830)
…affiliates.

## Summary

There were 8 different places where the copyright comment was wrong.
Rewrote from "Copyright (c) Facebook, Inc. and its affiliates." to
"Copyright (c) Meta Platforms, Inc. and its affiliates."

## How did you test this change?
No code was changed. Comment was still a comment after changes.

Co-authored-by: Dennis Moradkhani <denmo530@student.liu.se>
2023-05-19 17:52:17 -07:00
Sanket Singh 7bd330e0b0
Updated Copyright comment from Facebook to Meta (#26833)
## Summary
Changed the comment in react/packages/react
/react.shared-subset.js saying 
```
Copyright (c) Facebook, Inc. and affiliates ..
```
To 
```
Copyright (c) Meta Platforms, Inc. and affiliates ..
```
as raised in the following issues:
https://github.com/facebook/react/issues/26829

Files Changed: 
react/packages/react/react.shared-subset.js

## How did you test this change?

Tests Required: No
2023-05-19 17:51:44 -07:00
Sebastian Markbåge 5309f10285
Remove Flight Relay DOM/Native (#26828)
The bindings upstream in Relay has been removed so we don't need these
builds anymore. The idea is to revisit an FB integration of Flight but
it wouldn't use the Relay specific bindings. It's a bit unclear how it
would look but likely more like the OSS version so not worth keeping
these around.

The `dom-relay` name also included the FB specific Fizz implementation
of the streaming config so I renamed that to `dom-fb`. There's no Fizz
implementation for Native yet so I just removed `native-relay`.

We created a configurable fork for how to encode the output of Flight
and the Relay implementation encoded it as JSON objects instead of
strings/streams. The new implementation would likely be more stream-like
and just encode it directly as string/binary chunks. So I removed those
indirections so that this can just be declared inline in
ReactFlightServer/Client.
2023-05-17 20:33:25 -04:00
Ibrahem Mahyob d7a98a5e97
Fix strict mode badge URL (#26825)
## Summary
Closes https://github.com/facebook/react/issues/26821

[[Fix #26821]](https://github.com/facebook/react/issues/26821) Update
strict mode badge URL


Updated the URL in the strict mode badge to point to the correct React
documentation for StrictMode. The previous URL was outdated. Now, when a
component is not running in StrictMode, the badge links to
https://react.dev/reference/react/StrictMode for more information.


## How did you test this change?

I verified that the strict mode badge now correctly links to the updated
URL. Previously, it pointed to the outdated URL
(https://fb.me/devtools-strict-mode). After the update, it correctly
points to the React Dev documentation for StrictMode
(https://react.dev/reference/react/StrictMode).


_Since its my first contribution here, i have completed the CLA_
2023-05-17 13:34:34 +01:00
Ruslan Lesiutin 2468a87358
React DevTools 4.27.7 -> 4.27.8 (#26823)
Closes https://github.com/facebook/react/issues/26787,
https://github.com/facebook/react/issues/26793

Includes these changes:
* fix[devtools]: fixed duplicated backend activation with multiple
renderers ([hoxyq](https://github.com/hoxyq) in
[#26807](https://github.com/facebook/react/pull/26807))
2023-05-17 11:40:50 +01:00
Andrew Clark f8de255e94
Lower Suspense throttling heuristic to 300ms (#26803)
Now that the throttling mechanism applies more often, we've decided to
lower this a tad to ensure it's not noticeable. The idea is it should be
just large enough to prevent jank when lots of different parts of the UI
load in rapid succession, but not large enough to make the UI feel
sluggish. There's no perfect number, it's just a heuristic.
2023-05-16 15:59:15 -04:00
Andrew Clark 4bfcd02b2c
Fix Suspense throttling mechanism (#26802)
The throttling mechanism for fallbacks should apply to both their
appearance _and_ disappearance.

This was mostly addressed by #26611. See that PR for additional context.

However, a flaw in the implementation is that we only update the the
timestamp used for throttling when the fallback initially appears. We
don't update it when the real content pops in. If lots of content in
separate Suspense trees loads around the same time, you can still get
jank.

The issue is fixed by updating the throttling timestamp whenever the
visibility of a fallback changes. Not just when it appears.
2023-05-16 15:03:30 -04:00
Sophie Alpert 4cd7065665
Fix uSES hydration in strict mode (#26791)
Previously, we'd call and use getSnapshot on the second render resulting
in `Warning: Text content did not match. Server: "Nay!" Client: "Yay!"`
and then `Error: Text content does not match server-rendered HTML.`.

Fixes #26095. Closes #26113. Closes #25650.

---------

Co-authored-by: eps1lon <silbermann.sebastian@gmail.com>
2023-05-12 14:18:03 -07:00
Pieter De Baets a389046a52
[react-native] Always set RN$stopSurface (#26808)
## Summary
To support incremental adoption of bridgeless logic we want to default
to using these globals whenever they're available.

## How did you test this change?
https://github.com/facebook/react-native/pull/37410
2023-05-12 16:14:45 +01:00
Ruslan Lesiutin 67a05d03e3
fix[devtools]: fixed duplicated backend activation with multiple renderers (#26807)
## Summary
Initially reported in https://github.com/facebook/react/issues/26797.
Was not able to reproduce the exact same problem, but found this case:

1. Open corresponding codepen from the issue in debug mode
2. Open components tab of the extension
3. Refresh the page

Received multiple errors:
- Warning in the Console tab: Invalid renderer id "2".
- Error in the Components tab: Uncaught Error: Cannot add node "3"
because a node with that id is already in the Store.

This problem has occurred after landing a fix in
https://github.com/facebook/react/pull/26779. Looks like Chrome is
keeping the injected scripts (the backend in this case) and we start
backend twice.
2023-05-12 14:59:53 +01:00
Jan Kassens df12d7eac4
[www] reduce dynamic SchedulerFeatureFlags (#26617)
For these values we're not using dynamic values. We can statically
compile in the values we're running.
2023-05-10 17:26:05 -04:00
Andrew Clark 7cd98ef2bc
Fix nightly job to publish to "canary" channel (#26799)
When I was renaming the next channel to canary, I updated the
`publish_preleases` workflow correctly, but I skipped over
`publish_preleases_nightly`. Oops.
2023-05-09 22:27:35 -04:00
Andrew Clark b5810163e9
Change `yarn test` to default to experimental (#26741)
The idea is that the default `yarn test` command should be the one that
includes the most bleeding edge features, because during development you
probably want as many features enabled as possible.

That used to be `www-modern` but nowadays it's `experimental` because
we've landed a bunch of async actions stuff in experimental but it isn't
yet being tested at Meta.

So this switches the default to `experimental`.
2023-05-09 21:50:14 -04:00
Jan Kassens fda1f0b902
Flow upgrade to 0.205.1 (#26796)
Just a small upgrade to keep us current and remove unused suppressions
(probably fixed by some upgrade since).

- `*` is no longer allowed and has been an alias for `any` for a while
now.
2023-05-09 10:45:50 -04:00
Jan Kassens 7ac5e9a602
Small flowconfig fixes (#26784)
- The whole project root is included by default anyway, the include
section should be redundant and just misleading.
- The generated ignore paths ignore more than intended as they didn't
escape the `.` for regex.

Test Plan:
- wait for CI
- tested the ignore pattern change with renaming files and seeing the
expected files ignored for flow
2023-05-08 12:07:48 -04:00
Andrew Clark 16d053d592
Add useFormStatus to server rendering stub (#26788)
This was an oversight when I set up the hook in #26719.
2023-05-06 20:39:08 -04:00
dan efb381bbf9
[Release Script] Print a hint where to get the token (#26783)
I always forget where to get it.
2023-05-05 20:08:14 +01:00
Samuel Susla b00e27342d
Use native scheduler if defined in global scope (#26554)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2023-05-05 14:01:31 +01:00
Ruslan Lesiutin 783e7fcfa3
React DevTools 4.27.6 -> 4.27.7 (#26780)
List of changes:
* DevTools: fix backend activation ([hoxyq](https://github.com/hoxyq) in
[#26779](https://github.com/facebook/react/pull/26779))
* fix[dynamic-scripts-injection]: unregister content scripts before
registration ([hoxyq](https://github.com/hoxyq) in
[#26765](https://github.com/facebook/react/pull/26765))
2023-05-04 18:22:47 +01:00
Ruslan Lesiutin 377c5175f7
DevTools: fix backend activation (#26779)
## Summary
We have a case:
1. Open components tab
2. Close Chrome / Firefox devtools window completely
3. Reopen browser devtools panel
4. Open components tab

Currently, in version 4.27.6, we cannot load the components tree.

This PR contains two changes:
- non-functional refactoring in
`react-devtools-shared/src/devtools/store.js`: removed some redundant
type castings.
- fixed backend manager logic (introduced in
https://github.com/facebook/react/pull/26615) to activate already
registered backends. Looks like frontend of devtools also depends on
`renderer-attached` event, without it component tree won't load.

## How did you test this change?
This fixes the case mentioned prior. Currently in 4.27.6 version it is
not working, we need to refresh the page to make it work.

I've tested this in several environments: chrome, firefox, standalone
with RN application.
2023-05-04 17:34:57 +01:00
Sebastian Markbåge aef7ce5547
[Flight] Progressively Enhanced Server Actions (#26774)
This automatically exposes `$$FORM_ACTIONS` on Server References coming
from Flight. So that when they're used in a form action, we can encode
the ID for the server reference as a hidden field or as part of the name
of a button.

If the Server Action is a bound function it can have complex data
associated with it. In this case this additional data is encoded as
additional form fields.

To process a POST on the server there's now a `decodeAction` helper that
can take one of these progressive posts from FormData and give you a
function that is prebound with the correct closure and FormData so that
you can just invoke it.

I updated the fixture which now has a "Server State" that gets
automatically refreshed. This also lets us visualize form fields.
There's no "Action State" here for showing error messages that are not
thrown, that's still up to user space.
2023-05-03 18:36:57 -04:00
Sebastian Markbåge c10010a6a0
[Fizz] Gracefully handle suspending in DOM configs (#26768)
E.g. if we suspend (throw a promise) in pushStartInstance today we might
have already pushed some chunks (or even child segments potentially). We
should revert back to where we were.

This doesn't usually happen because when we suspend in a component it
doesn't write anything itself, it'll always defer to som host instance
to do the writing.

There was a todo about this already but I'm not 100% sure it's always
safe when suspending. It should be safe when suspending just regularly
because it's just a noop. We might not even want "throwing a promise" in
this mechanism to be supported longer term but for now that's how a
suspend in internals.
2023-05-03 18:13:47 -04:00
Andrew Clark f533cee8cb
Add useFormStatus to Flight fixture (#26773) 2023-05-03 15:29:32 -04:00
Andrew Gadzik 2c1117a8d0
Reuse request so that a ReabableStream body does not become disturbed (#26771) 2023-05-03 15:06:10 -04:00
Sebastian Markbåge fa7a447b9c
[Fizz] Check for nullish values on ReactCustomFormAction (#26770)
Usually we don't have to do this since we only set these in the loop but
the ReactCustomFormAction props are optional so they might be undefined.

Also moved it to a general type since it's a semi-public API.
2023-05-03 14:35:26 -04:00
Andrew Clark b7972822b5
useOptimisticState -> useOptimistic (#26772)
Drop the "state". Just "useOptimistic". It's cleaner.

This is still an experimental API. May not be the final name.
2023-05-03 14:26:00 -04:00
Andrew Clark 388686f291
Add "canary" to list of allowed npm dist tags (#26767)
Forgot this allowlist existed. It's an extra safeguard, in case we mess
up the configuration somehow.
2023-05-03 12:36:34 -04:00
Ruslan Lesiutin 8a25302c66
fix[dynamic-scripts-injection]: unregister content scripts before registration (#26765)
## Summary
Fixes #26756.

DevTools is failing to inject `__REACT_DEVTOOLS_GLOBAL_HOOK__` hook in
incognito mode. This is not happening straight-forward, but if extension
is toggled on and off, the next time I try to open it I am receiving an
error that content script was already registered.

<img width="676" alt="Screenshot 2023-05-02 at 14 36 53"
src="https://user-images.githubusercontent.com/28902667/235877692-51c5d284-79d9-4b00-b62e-d25d5bb5e056.png">

- Unregistering content scripts before attempting to register them
again. We need to inject `__REACT_DEVTOOLS_GLOBAL_HOOK__` on each page,
so this should be expected behaviour.
- Fixed error logging
 
## How did you test this change?
Local build of extension for Chrome, trying the same steps, which
resulted in an error.
No regression in performance, tested on react.dev, still the same.
2023-05-03 17:27:39 +01:00
Andrew Clark 2c2476834a
Rename "next" prerelease channel to "canary" (#26761)
The "next" prerelease channel represents what will be published the next
time we do a stable release. We publish a new "next" release every day
using a timed CI workflow.

When we introduced this prerelease channel a few years ago, another name
we considered was "canary". But I proposed "next" instead to create a
greater distinction between this channel and the "experimental" channel
(which is published at the same cadence, but includes extra experimental
features), because some other projects use "canary" to refer to releases
that are more unstable than how we would use it.

The main downside of "next" is someone might mistakenly assume the name
refers to Next.js. We were aware of this risk at the time but didn't
think it would be an issue in practice.

However, colloquially, we've ended up referring to this as the "canary"
channel anyway to avoid precisely that confusion.

So after further discussion, we've agreed to rename to "canary".

This affects the label used in the version string (e.g.
`18.3.0-next-a1c2d3e4` becomes `18.3.0-canary-a1c2d3e4`) as well as the
npm dist tags used to publish the releases. For now, I've chosen to
publish the canaries using both `@canary` and `@next` dist tags, so that
downstream consumers who might depend on `@next` have time to adjust. We
can remove that later after the change has been communicated.
2023-05-03 12:10:32 -04:00
Andrew Clark fa4314841e
Remove deprecated workflow key from Circle config (#26762)
This key was only valid during the 2.0 beta period so we can remove it.
2023-05-02 14:59:36 -04:00
Samuel Susla 5dd90c5623
Use content hash for react-native builds (#26734) 2023-05-02 12:04:02 +01:00
Sebastian Markbåge 559e83aebb
[Fizz] Allow an action provide a custom set of props to use for progressive enhancement (#26749)
Stacked on top of #26735.

This allows a framework to add a `$$FORM_ACTION` property to a function.
This lets the framework return a set of props to use in place of the
function but only during SSR. Effectively, this lets you implement
progressive enhancement of form actions using some other way instead of
relying on the replay feature.

This will be used by RSC on Server References automatically by
convention in a follow up, but this mechanism can also be used by other
frameworks/libraries.
2023-05-01 16:01:14 -04:00
Sebastian Markbåge 67f4fb0213
Allow forms to skip hydration of hidden inputs (#26735)
This allows us to emit extra ephemeral data that will only be used on
server rendered forms.

First I refactored the shouldSkip functions to now just do that work
inside the canHydrate methods. This makes the Config bindings a little
less surface area but it also helps us optimize a bit since we now can
look at the code together and find shared paths.

canHydrate returns the instance if it matches, that used to just be
there to refine the type but it can also be used to just return a
different instance later that we find. If we don't find one, we'll bail
out and error regardless so no need to skip past anything.
2023-05-01 15:35:57 -04:00
Josh Story 8ea96ef84d
[Fizz] Encode external fizz runtime into chunks eagerly (#26752)
in https://github.com/facebook/react/pull/26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.
2023-05-01 10:50:36 -07:00
Andrew Clark 491aec5d61
Implement experimental_useOptimisticState (#26740)
This adds an experimental hook tentatively called useOptimisticState.
(The actual name needs some bikeshedding.)

The headline feature is that you can use it to implement optimistic
updates. If you set some optimistic state during a transition/action,
the state will be automatically reverted once the transition completes.

Another feature is that the optimistic updates will be continually
rebased on top of the latest state.

It's easiest to explain with examples; we'll publish documentation as
the API gets closer to stabilizing. See tests for now.

Technically the use cases for this hook are broader than just optimistic
updates; you could use it implement any sort of "pending" state, such as
the ones exposed by useTransition and useFormStatus. But we expect
people will most often reach for this hook to implement the optimistic
update pattern; simpler cases are covered by those other hooks.
2023-05-01 13:19:20 -04:00
Dan Ott 9545e4810c
Add nonce support to bootstrap scripts and external runtime (#26738)
Adds support for nonce on `bootstrapScripts`, `bootstrapModules` and the external fizz runtime
2023-05-01 09:19:02 -07:00
Andrew Clark 86b0e91998
Gate DevTools test to fix CI (#26742)
This test started failing recently in older versions of React because
the Scheduler priority inside a microtask is Normal instead of
Immediate. This is expected because microtasks are not Scheduler tasks;
it's an implementation detail.

I gated the test to only run in v17 because it's a regression test for
legacy Suspense behavior, and the implementation details of the snapshot
changed in v18.

Test plan
---------

Using latest:

```
yarn test --build --project devtools --release-channel=experimental profilingcache
```

Using v17 (typically runs in a timed CI workflow):

```
/scripts/circleci/download_devtools_regression_build.js 17.0 --replaceBuild
yarn test --build --project devtools --release-channel=experimental --reactVersion 17.0 profilingcache
```
2023-04-29 11:14:16 -04:00
Josh Story b12bea62d9
Preinits should support a nonce option (#26744)
Currently there is no way to provide a nonce when using
`ReactDOM.preinit(..., { as: 'script' })`

This PR adds `nonce?: string` as an option

While implementing this PR I added a test to also show you can pass
`integrity`. This test isn't directly related to the nonce change.
2023-04-28 15:57:39 -07:00
Sebastian Silbermann efbd68511d
Remove unused `initialStatus` parameter from `useHostTransitionStatus` (#26743) 2023-04-28 23:18:10 +02:00
Andrew Clark 18282f881d
Fix: Update while suspended fails to interrupt (#26739)
This fixes a bug with `use` where if you update a component that's
currently suspended, React will sometimes mistake it for a render phase
update.

This happens because we don't reset `currentlyRenderingFiber` until the
suspended is unwound. And with `use`, that can happen asynchronously,
most commonly when the work loop is suspended during a transition.

The fix is to make sure `currentlyRenderingFiber` is only set when we're
in the middle of rendering, which used to be true until `use` was
introduced.

More specifically this means clearing `currentlyRenderingFiber` when
something throws and setting it again when we resume work.

In many cases, this bug will fail "gracefully" because the update is
still added to the queue; it's not dropped completely. It's also
somewhat rare because it has to be the exact same component that's
currently suspended. But it's still a bug. I wrote a regression test
that shows a sync update failing to interrupt a suspended component.
2023-04-28 15:35:14 -04:00
Andrew Clark 540bab085d
Implement experimental_useFormStatus (#26722)
This hook reads the status of its ancestor form component, if it exists.

```js
const {pending, data, action, method} = useFormStatus();
```

It can be used to implement a loading indicator, for example. You can
think of it as a shortcut for implementing a loading state with the
useTransition hook.

For now, it's only available in the experimental channel. We'll share
docs once its closer to being stable. There are additional APIs that
will ship alongside it.

Internally it's implemented using startTransition + a context object.
That's a good way to think about its behavior, but the actual
implementation details may change in the future.

Because form elements cannot be nested, the implementation in the
reconciler does not bother to keep track of multiple nested "transition
providers". So although it's implemented using generic Fiber config
methods, it does currently make some assumptions based on React DOM's
requirements.
2023-04-26 18:19:58 -04:00
Andrew Clark 6eadbe0c4a
Fix: Resolve entangled actions independently (#26726)
When there are multiple async actions at the same time, we entangle them
together because we can't be sure which action an update might be
associated with. (For this, we'd need AsyncContext.) However, if one of
the async actions fails with an error, it should only affect that
action, not all the other actions it may be entangled with.

Resolving each action independently also means they can have independent
pending state types, rather than being limited to an `isPending`
boolean. We'll use this to implement an upcoming form API.
2023-04-25 20:43:20 -04:00
Josh Story ec5e9c2a75
Fix double preload (#26729)
I found a couple scenarios where preloads were issued too aggressively

1. During SSR, if you render a new stylesheet after the preamble flushed
it will flush a preload even if the resource was already preloaded
2. During Client render, if you call `ReactDOM.preload()` it will only
check if a preload exists in the Document before inserting a new one. It
should check for an underlying resource such as a stylesheet link or
script if the preload is for a recognized asset type
2023-04-25 15:10:34 -07:00
Rubén Norte f87e97a0a6
Handle line endings correctly on Windows in build script for RN (#26727)
## Summary

We added some post-processing in the build for RN in #26616 that broke
for users on Windows due to how line endings were handled to the regular
expression to insert some directives in the docblock. This fixes that
problem, reported in #26697 as well.

## How did you test this change?

Verified files are still built correctly on Mac/Linux. Will ask for help
to test on Windows.
2023-04-25 17:26:34 +01:00
lauren 25b99efe0c
[DevTools] Add support for useMemoCache (#26696)
useMemoCache wasn't previously supported in the DevTools, so any attempt
to inspect a component using the hook would result in a
`dispatcher.useMemoCache is not a function (it is undefined)` error.
2023-04-25 09:19:25 -07:00
Andrew Clark ed545ae3d3
Turn off enableFormActions in Meta build (#26721)
This is enabled in the canary channels, but because it's relatively
untested, we'll disable it at Meta until they're ready to start trying
it out. It can change some behavior even if you don't intentionally
start using the API.

The reason it's not a dynamic flag is that it affects the external Fizz
runtime, which currently can't read flags at runtime.
2023-04-25 12:01:34 -04:00
Sebastian Markbåge bf449ee74e
Replay Client Actions After Hydration (#26716)
We used to have Event Replaying for any kind of Discrete event where
we'd track any event after hydrateRoot and before the async code/data
has loaded in to hydrate the target. However, this didn't really work
out because code inside event handlers are expected to be able to
synchronously read the state of the world at the time they're invoked.
If we replay discrete events later, the mutable state around them like
selection or form state etc. may have changed.

This limitation doesn't apply to Client Actions:

- They're expected to be async functions that themselves work
asynchronously. They're conceptually also in the "navigation" events
that happen after the "submit" events so they're already not
synchronously even before the first `await`.
- They're expected to operate mostly on the FormData as input which we
can snapshot at the time of the event.

This PR adds a bit of inline script to the Fizz runtime (or external
runtime) to track any early submit events on the page - but only if the
action URL is our placeholder `javascript:` URL. We track a queue of
these on `document.$$reactFormReplay`. Then we replay them in order as
they get hydrated and we get a handle on the Client Action function.

I add the runtime to the `bootstrapScripts` phase in Fizz which is
really technically a little too late, because on a large page, it might
take a while to get to that script even if you have displayed the form.
However, that's also true for external runtime. So there's a very short
window we might miss an event but it's good enough and better than
risking blocking display on this script.

The main thing that makes the replaying difficult to reason about is
that we can have multiple instance of React using this same queue. This
would be very usual but you could have two different Reacts SSR:ing
different parts of the tree and using around the same version. We don't
have any coordinating ids for this. We could stash something on the form
perhaps but given our current structure it's more difficult to get to
the form instance in the commit phase and a naive solution wouldn't
preserve ordering between forms.

This solution isn't 100% guaranteed to preserve ordering between
different React instances neither but should be in order within one
instance which is the common case.

The hard part is that we don't know what instance something will belong
to until it hydrates. So to solve that I keep everything in the original
queue while we wait, so that ordering is preserved until we know which
instance it'll go into. I ended up doing a bunch of clever tricks to
make this work. These could use a lot more tests than I have right now.

Another thing that's tricky is that you can update the action before
it's replayed but we actually want to invoke the old action if that
happens. So we have to extract it even if we can't invoke it right now
just so we get the one that was there during hydration.
2023-04-25 10:22:20 -04:00
Sebastian Markbåge 64d6be7122
Use require() to implement script src in tests (#26717)
We currently use rollup to make an adhoc bundle from the file system
when we're testing an import of an external file.

This doesn't follow all the interception rules that we use in jest and
in our actual builds.

This switches to just using jest require() to load these. This means
that they effectively have to load into the global document so this only
works with global document tests which is all we have now anyway.
2023-04-25 09:12:16 -04:00
Andrew Clark 919620b293
Add stub for experimental_useFormStatus (#26719)
This wires up, but does not yet implement, an experimental hook called
useFormStatus. The hook is imported from React DOM, not React, because
it represents DOM-specific state — its return type includes FormData as
one of its fields. Other renderers that implement similar methods would
use their own renderer-specific types.

The API is prefixed and only available in the experimental channel.

It can only be used from client (browser, SSR) components, not Server
Components.
2023-04-24 20:18:34 -04:00
Sebastian Markbåge 9ece58ebaa
Go through the toString path for booleanish strings and .name property (#26720)
This is consistent with what we used to do but not what we want to do.
2023-04-24 19:26:50 -04:00
Sebastian Markbåge 5e5342b100
Insert temporary input node to polyfill submitter argument in FormData (#26714)
Insert temporary input node to polyfill submitter argument in FormData.
This works for buttons too and fixes a bug where the type attribute
wasn't reset.

I also exclude the submitter if it's a function action. This ensures
that we don't include the generated "name" when the action is a server
action. Conceptually that name doesn't exist.
2023-04-24 14:18:09 -04:00
Sebastian Markbåge 9c58a0b647
Update Flight fixture to use use() instead of Promise as a child (#26715)
The Promise as a child case seems buggy. It ends up throwing the Promise
as fatal when used in Sync rendering.
2023-04-24 14:00:25 -04:00
Sophie Alpert 9ee7964302
Fix escaping in ReactDOMInput code (#26630)
JSON.stringify isn't the right thing here. Luckily this doesn't look to have any security impact.
2023-04-24 10:33:11 -07:00
Sebastian Markbåge 2fa6323818
Restore server controlled form fields to whatever they should be (#26708)
Fizz can emit whatever it wants for the SSR version of these fields when
it's a function action so they might not align with what is in the
previous props. Therefore we need to force them to update if we're
updating to a non-function where they might be relevant again.
2023-04-23 17:44:28 -04:00
Andrew Clark 7ce765ec32
Clean up enableUseHook flag (#26707)
This has been statically enabled everywhere for months.
2023-04-23 14:50:17 -04:00
Sebastian Markbåge a21d1475ff
[Flight] Fix File Upload in Node.js (#26700)
Use the Blob constructor + append with filename instead of File
constructor. Node.js doesn't expose a global File constructor but does
support it in this form.

Queue fields until we get the 'end' event from the previous file. We
rely on previous files being available by the time a field is resolved.
However, since the 'end' event in Readable is fired after two
micro-tasks, these are not resolved in order.

I use a queue of the fields while we're still waiting on files to
finish. This still doesn't resolve files and fields in order relative to
each other but that doesn't matter for our usage.
2023-04-22 01:04:24 -04:00
Josh Story 36e4cbe2e9
[Float][Flight] Flight support for Float (#26502)
Stacked on #26557 

Supporting Float methods such as ReactDOM.preload() are challenging for
flight because it does not have an easy means to convey direct
executions in other environments. Because the flight wire format is a
JSON-like serialization that is expected to be rendered it currently
only describes renderable elements. We need a way to convey a function
invocation that gets run in the context of the client environment
whether that is Fizz or Fiber.

Fiber is somewhat straightforward because the HostDispatcher is always
active and we can just have the FlightClient dispatch the serialized
directive.

Fizz is much more challenging becaue the dispatcher is always scoped but
the specific request the dispatch belongs to is not readily available.
Environments that support AsyncLocalStorage (or in the future
AsyncContext) we will use this to be able to resolve directives in Fizz
to the appropriate Request. For other environments directives will be
elided. Right now this is pragmatic and non-breaking because all
directives are opportunistic and non-critical. If this changes in the
future we will need to reconsider how widespread support for async
context tracking is.

For Flight, if AsyncLocalStorage is available Float methods can be
called before and after await points and be expected to work. If
AsyncLocalStorage is not available float methods called in the sync
phase of a component render will be captured but anything after an await
point will be a noop. If a float call is dropped in this manner a DEV
warning should help you realize your code may need to be modified.

This PR also introduces a way for resources (Fizz) and hints (Flight) to
flush even if there is not active task being worked on. This will help
when Float methods are called in between async points within a function
execution but the task is blocked on the entire function finishing.

This PR also introduces deduping of Hints in Flight using the same
resource keys used in Fizz. This will help shrink payload sizes when the
same hint is attempted to emit over and over again
2023-04-21 20:45:51 -07:00
Andrew Clark 8f42196892
Change DOM HostContext to number instead of string (#26698)
In React DOM, we use HostContext to represent the namespace of whatever
is currently rendering — SVG, Math, or HTML. Because there is a fixed
set of possible values, we can switch this to be a number instead. My
motivation is that I want to start tracking additional information in
this type, and I want to pack all of it into a single number instead of
turning it into an object. For better performance.

(In dev, the host context type is already an object that includes
additional information, but that's dev so who cares.)

Technically, before this change, the host context could be any namespace
URI string, but any value other than SVG or Math was treated the same
way. Only SVG and Math have special behavior. So in the new structure,
there are three enum values: SVG, Math, or None, which represents the
HTML namespace as well as all other possible namespaces.
2023-04-21 21:44:43 -04:00
Andrew Clark 967d46c76c
Add error boundary to Flight fixture (#26695)
Errors in form actions are now rethrown during render (#26689), so we
can handle them using an error boundary.
2023-04-21 14:23:50 -04:00
Jan Kassens 5d7ebb4b78
Allow Node.js 20 to build (#26693)
This is stable and appears to build w/o problem. I don't see why we
should disallow it.
2023-04-21 19:23:27 +01:00
Andrew Clark fd3fb8e3c5
Rethrow errors from form actions (#26689)
This is the next step toward full support for async form actions.

Errors thrown inside form actions should cause the form to re-render and
throw the error so it can be captured by an error boundary. The behavior
is the same if the `<form />` had an internal useTransition hook, which
is pretty much exactly how we implement it, too.

The first time an action is called, the form's HostComponent is
"upgraded" to become stateful, by lazily mounting a list of hooks. The
rest of the implementation for function components can be shared.

Because the error handling behavior added in this commit is just using
useTransition under-the-hood, it also handles pending states, too.
However, this pending state can't be observed until we add a new hook
for that purpose. I'll add this next.
2023-04-21 13:29:46 -04:00
Jan Kassens c57a0f68a4
Remove react-is download hack (#26692)
This was added during an upgrade to Jest 24 in
https://github.com/facebook/react/pull/15778

By now we're at Jest 29. I think if CI passes we might not need this
hack anymore.
2023-04-21 13:11:09 -04:00
Itai Mizlish c8369527ef
update broken icon link (circleCi status) (#26688)
looks better now 🚀
2023-04-20 18:28:45 -07:00
Josh Story cc93a85332
[Fiber] InvokeGuardedCallback without metaprogramming (#26569)
InvokeGuardedCallback is now implemented with the browser fork done at
error-time rather than module-load-time. Originally it also tried to
freeze the window/document references to avoid mismatches in prototype
chains when testing React in different documents however we have since
updated our tests to not do this and it was a test only feature so I
removed it.
2023-04-20 15:08:51 -07:00
Josh Story fdad813ac7
[Float][Fiber] Enable Float methods to be called outside of render (#26557)
Stacked on #26570 

Previously we restricted Float methods to only being callable while
rendering. This allowed us to make associations between calls and their
position in the DOM tree, for instance hoisting preinitialized styles
into a ShadowRoot or an iframe Document.

When considering how we are going to support Flight support in Float
however it became clear that this restriction would lead to compromises
on the implementation because the Flight client does not execute within
the context of a client render. We want to be able to disaptch Float
directives coming from Flight as soon as possible and this requires
being able to call them outside of render.

this patch modifies Float so that its methods are callable anywhere. The
main consequence of this change is Float will always use the Document
the renderer script is running within as the HoistableRoot. This means
if you preinit as style inside a component render targeting a ShadowRoot
the style will load in the ownerDocument not the ShadowRoot. Practially
speaking it means that preinit is not useful inside ShadowRoots and
iframes.

This tradeoff was deemed acceptable because these methods are
optimistic, not critical. Additionally, the other methods, preconntect,
prefetchDNS, and preload, are not impacted because they already operated
at the level of the ownerDocument and really only interface with the
Network cache layer.

I added a couple additional fixes that were necessary for getting tests
to pass that are worth considering separately.

The first commit improves the diff for `waitForThrow` so it compares
strings if possible.

The second commit makes invokeGuardedCallback not use metaprogramming
pattern and swallows any novel errors produced from trying to run the
guarded callback. Swallowing may not be the best we can do but it at
least protects React against rapid failure when something causes the
dispatchEvent to throw.
2023-04-20 14:40:25 -07:00
Josh Story e5708b3ea9
[Tests][Fizz] Better HTML parsing behavior for Fizz tests (#26570)
In anticipation of making Fiber use the document global for dispatching
Float methods that arrive from Flight I needed to update some tests that
commonly recreated the JSDOM instance after importing react.

This change updates a few tests to only create JSDOM once per test,
before importing react-dom/client.

Additionally the current act implementation for server streaming did not
adequately model streaming semantics so I rewrite the act implementation
in a way that better mirrors how a browser would parse incoming HTML.

The new act implementation does the following

1. the first time it processes meaningful streamed content it figures
out whether it is rendering into the existing document container or if
it needs to reset the document. this is based on whether the streamed
content contains tags `<html>` or `<body>` etc...
2. Once the streaming container is set it will typically continue to
stream into that container for future calls to act. The exception is if
the streaming container is the `<head>` in which case it will switch to
streaming into the body once it receives a `<body>` tag.

This means for tests that render something like a `<div>...</div>` it
will naturally stream into the default `<div id="container">...` and for
tests that render a full document the HTML will parse like a real
browser would (with some very minor edge case differences)

I also refactored the way we move nodes from buffered content into the
document and execute any scripts we find. Previously we were using
window.eval and I switched this to just setting the external script
content as script text. Additionally the nonce logic is reworked to be a
bit simpler.
2023-04-20 14:27:02 -07:00
Andrew Clark d73d7d5908
Add `alwaysThrottleRetries` flag (#26685)
This puts the change introduced by #26611 behind a flag until Meta is
able to roll it out. Disabling the flag reverts back to the old
behavior, where retries are throttled if there's still data remaining in
the tree, but not if all the data has finished loading.

The new behavior is still enabled in the public builds.
2023-04-20 14:23:22 -04:00
Ruslan Lesiutin 7f8c501f68
React DevTools 4.27.5 -> 4.27.6 (#26684)
Full list of changes:
* Use .slice() for all substring-ing
([sophiebits](https://github.com/sophiebits) in
[#26677](https://github.com/facebook/react/pull/26677))
* cleanup[devtools]: remove named hooks & profiler changed hook indices
feature flags ([hoxyq](https://github.com/hoxyq) in
[#26635](https://github.com/facebook/react/pull/26635))
* chore[devtools/release-scripts]: update messages / fixed npm view com…
([hoxyq](https://github.com/hoxyq) in
[#26660](https://github.com/facebook/react/pull/26660))
* (patch)[DevTools] bug fix: backend injection logic not working for
undocked devtools window ([mondaychen](https://github.com/mondaychen) in
[#26665](https://github.com/facebook/react/pull/26665))
* use backend manager to support multiple backends in extension
([mondaychen](https://github.com/mondaychen) in
[#26615](https://github.com/facebook/react/pull/26615))
2023-04-20 13:34:25 +01:00
Sophie Alpert 22d5942675
Add two event system cleanup TODOs (#26678)
There is so much old stuff in these files. I am weeping.
2023-04-19 18:41:46 -07:00
Sophie Alpert 767f52237c
Use .slice() for all substring-ing (#26677)
- substr is Annex B
- substring silently flips its arguments if they're in the "wrong order", which is confusing
- slice is better than sliced bread (no pun intended) and also it works the same way on Arrays so there's less to remember

---

> I'd be down to just lint and enforce a single form just for the potential compression savings by using a repeated string.

_Originally posted by @sebmarkbage in https://github.com/facebook/react/pull/26663#discussion_r1170455401_
2023-04-19 14:26:01 -07:00
Sebastian Markbåge c826dc50de
Add (Client) Functions as Form Actions (#26674)
This lets you pass a function to `<form action={...}>` or `<button
formAction={...}>` or `<input type="submit formAction={...}>`. This will
behave basically like a `javascript:` URL except not quite implemented
that way. This is a convenience for the `onSubmit={e => {
e.preventDefault(); const fromData = new FormData(e.target); ... }`
pattern.

You can still implement a custom `onSubmit` handler and if it calls
`preventDefault`, it won't invoke the action, just like it would if you
used a full page form navigation or javascript urls. It behaves just
like a navigation and we might implement it with the Navigation API in
the future.

Currently this is just a synchronous function but in a follow up this
will accept async functions, handle pending states and handle errors.

This is implemented by setting `javascript:` URLs, but these only exist
to trigger an error message if something goes wrong instead of
navigating away. Like if you called `stopPropagation` to prevent React
from handling it or if you called `form.submit()` instead of
`form.requestSubmit()` which by-passes the `submit` event. If CSP is
used to ban `javascript:` urls, those will trigger errors when these
URLs are invoked which would be a different error message but it's still
there to notify the user that something went wrong in the plumbing.

Next up is improving the SSR state with action replaying and progressive
enhancement.
2023-04-19 16:31:08 -04:00
Andrew Clark cd2b79dedd
Initial (client-only) async actions support (#26621)
Implements initial (client-only) support for async actions behind a
flag. This is an experimental feature and the design isn't completely
finalized but we're getting closer. It will be layered alongside other
features we're working on, so it may not feel complete when considered
in isolation.

The basic description is you can pass an async function to
`startTransition` and all the transition updates that are scheduled
inside that async function will be grouped together. The `isPending`
flag will be set to true immediately, and only set back to false once
the async action has completed (as well as all the updates that it
triggers).

The ideal behavior would be that all updates spawned by the async action
are automatically inferred and grouped together; however, doing this
properly requires the upcoming (stage 2) Async Context API, which is not
yet implemented by browsers. In the meantime, we will fake this by
grouping together all transition updates that occur until the async
function has terminated. This can lead to overgrouping between unrelated
actions, which is not wrong per se, just not ideal.

If the `useTransition` hook is removed from the UI before an async
action has completed — for example, if the user navigates to a new page
— subsequent transitions will no longer be grouped with together with
that action.

Another consequence of the lack of Async Context is that if you call
`setState` inside an action but after an `await`, it must be wrapped in
`startTransition` in order to be grouped properly. If we didn't require
this, then there would be no way to distinguish action updates from
urgent updates caused by user input, too. This is an unfortunate footgun
but we can likely detect the most common mistakes using a lint rule.

Once Async Context lands in browsers, we can start warning in dev if we
detect an update that hasn't been wrapped in `startTransition`. Then,
longer term, once the feature is ubiquitous, we can rely on it for real
and allow you to call `setState` without the additional wrapper.

Things that are _not_ yet implemented in this PR, but will be added as
follow ups:

- Support for non-hook form of `startTransition`
- Canceling the async action scope if the `useTransition` hook is
deleted from the UI
- Anything related to server actions
2023-04-19 13:33:11 -04:00
Jan Kassens 6d394e3d26
[actions] commit from special branches iff they exist (#26673)
This creates 2 special branches. If these special branches exist, we'll
commit build artifacts from these branches, main otherwise.
2023-04-19 12:11:40 -04:00
Sebastian Markbåge 1f248bdd71
Switching checked to null should leave the current value (#26667)
I accidentally made a behavior change in the refactor. It turns out that
when switching off `checked` to an uncontrolled component, we used to
revert to the concept of "initialChecked" which used to be stored on
state.

When there's a diff to this computed prop and the value of props.checked
is null, then we end up in a case where it sets `checked` to
`initialChecked`:


5cbe6258bc/packages/react-dom-bindings/src/client/ReactDOMInput.js (L69)

Since we never changed `initialChecked` and it's not relevant if
non-null `checked` changes value, the only way this "change" could
trigger was if we move from having `checked` to having null.

This wasn't really consistent with how `value` works, where we instead
leave the current value in place regardless. So this is a "bug fix" that
changes `checked` to be consistent with `value` and just leave the
current value in place. This case should already have a warning in it
regardless since it's going from controlled to uncontrolled.

Related to that, there was also another issue observed in
https://github.com/facebook/react/pull/26596#discussion_r1162295872 and
https://github.com/facebook/react/pull/26588

We need to atomically apply mutations on radio buttons. I fixed this by
setting the name to empty before doing mutations to value/checked/type
in updateInput, and then set the name to whatever it should be. Setting
the name is what ends up atomically applying the changes.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>
2023-04-19 11:46:29 -04:00
Ruslan Lesiutin b90e8ebaa5
cleanup[devtools]: remove named hooks & profiler changed hook indices feature flags (#26635)
## Summary

Removing `enableNamedHooksFeature`, `enableProfilerChangedHookIndices`,
`enableProfilerComponentTree` feature flags, they are the same for all
configurations.
2023-04-19 10:05:31 +01:00
Ruslan Lesiutin a227bcd4f4
chore[devtools/release-scripts]: update messages / fixed npm view com… (#26660)
Some minor changes, observed while working on 24.7.5 release:
- Updated numeration of text instructions
- `reactjs.org` -> `react.dev`
- Fixed using `npm view` command for node 16+, `publish-release` script
currently fails if used with node 16+
2023-04-19 10:05:16 +01:00
Sophie Alpert c6db19f9cd
[Flight] Serialize Date (#26622)
This is kind of annoying because Date implements toJSON so
JSON.stringify turns it into a string before calling our replacer
function.
2023-04-18 20:52:03 -07:00
Mengdi Chen 96fd2fb726
(patch)[DevTools] bug fix: backend injection logic not working for undocked devtools window (#26665)
bugfix for #26492

This bug would cause users unable to use the devtools (component tree
empty).

The else-if logic is broken when user switch to undocked devtools mode
(separate window) because `sender.tab` would exist in that case.
<img width="314" alt="image"
src="https://user-images.githubusercontent.com/1001890/232930094-05a74445-9189-4d50-baf1-a0360b29ef7e.png">

Tested on Chrome with a local build
2023-04-18 20:39:22 -04:00
Sebastian Markbåge d8089f2cf2
[Flight Reply] Encode FormData (#26663)
Builds on top of https://github.com/facebook/react/pull/26661

This lets you pass FormData objects through the Flight Reply
serialization. It does that by prefixing each entry with the ID of the
reference and then the decoding side creates a new FormData object
containing only those fields (without the prefix).

Ideally this should be more generic. E.g. you should be able to pass
Blobs, Streams and Typed Arrays by reference inside plain objects too.
You should also be able to send Blobs and FormData in the regular Flight
serialization too so that they can go both directions. They should be
symmetrical. We'll get around to adding more of those features in the
Flight protocol as we go.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>
2023-04-18 14:57:33 -04:00
Sophie Alpert 1b4a0daba8
Add assertions about <input> value dirty state (#26626)
Since this is an observable behavior and is hard to think about, seems
good to have tests for this.

The expected value included in each test is the behavior that existed
prior to #26546.
2023-04-18 11:03:36 -07:00
Sophie Alpert b433c379d5
Fix input tracking bug (#26627)
In
2019ddc75f,
we changed to set .defaultValue before .value on updates. In some cases,
setting .defaultValue causes .value to change, and since we only set
.value if it has the wrong value, this resulted in us not assigning to
.value, which resulted in inputValueTracking not knowing the right
value. See new test added.

My fix here is to (a) move the value setting back up first and (b)
narrowing the fix in the aforementioned PR to newly remove the value
attribute only if it defaultValue was previously present in props.

The second half is necessary because for types where the value property
and attribute are indelibly linked (hidden checkbox radio submit image
reset button, i.e. spec modes default or default/on from
https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default),
we can't remove the value attribute after setting .value, because that
will undo the assignment we just did! That is, not having (b) makes all
of those types fail to handle updating props.value.

This code is incredibly hard to think about but I think this is right
(or at least, as right as the old code was) because we set .value here
only if the nextProps.value != null, and we now remove defaultValue only
if lastProps.defaultValue != null. These can't happen at the same time
because we have long warned if value and defaultValue are simultaneously
specified, and also if a component switches between controlled and
uncontrolled.

Also, it fixes the test in https://github.com/facebook/react/pull/26626.
2023-04-18 10:49:32 -07:00
Mengdi Chen d962f35cac
[DevTools] use backend manager to support multiple backends in extension (#26615)
In the extension, currently we do the following:
1. check whether there's at least one React renderer on the page
2. if yes, load the backend to the page
3. initialize the backend 

To support multiple versions of backends, we are changing it to:
1. check the versions of React renders on the page
2. load corresponding React DevTools backends that are shipped with the
extension; if they are not contained (usually prod builds of
prereleases), show a UI to allow users to load them from UI
3. initialize each of the backends

To enable this workflow, a backend will ignore React renderers that does
not match its version

This PR adds a new file "backendManager" in the extension for this
purpose.


------
I've tested it on Chrome, Edge and Firefox extensions
2023-04-18 12:02:42 -04:00
Ruslan Lesiutin 77d3b02e5c
React DevTools 4.27.4 -> 4.27.5 (#26637)
Full list of changes (not everything included in changelog):
* refactor[devtools]: copy to clipboard only on frontend side
([hoxyq](https://github.com/hoxyq) in
[#26604](https://github.com/facebook/react/pull/26604))
* Provide icon to edge devtools.
([harrygz889](https://github.com/harrygz889) in
[#26543](https://github.com/facebook/react/pull/26543))
* [BE] move shared types & constants to consolidated locations
([mondaychen](https://github.com/mondaychen) in
[#26572](https://github.com/facebook/react/pull/26572))
* remove backend dependency from the global hook
([mondaychen](https://github.com/mondaychen) in
[#26563](https://github.com/facebook/react/pull/26563))
* Replace deprecated `new-window` with
`webContents.setWindowOpenHandler()`
([Willie-Boy](https://github.com/Willie-Boy) in
[#26559](https://github.com/facebook/react/pull/26559))
* DevTools: Inline references to fiber flags
([acdlite](https://github.com/acdlite) in
[#26542](https://github.com/facebook/react/pull/26542))
* refactor[devtools]: forbid editing class instances in props
([hoxyq](https://github.com/hoxyq) in
[#26522](https://github.com/facebook/react/pull/26522))
* Move update scheduling to microtask
([acdlite](https://github.com/acdlite) in
[#26512](https://github.com/facebook/react/pull/26512))
* Remove unnecessary CIRCLE_CI_API_TOKEN checks
([mondaychen](https://github.com/mondaychen) in
[#26499](https://github.com/facebook/react/pull/26499))
* browser extension: improve script injection logic
([mondaychen](https://github.com/mondaychen) in
[#26492](https://github.com/facebook/react/pull/26492))
* [flow] make Flow suppressions explicit on the error
([kassens](https://github.com/kassens) in
[#26487](https://github.com/facebook/react/pull/26487))
2023-04-17 17:42:02 +01:00
Sebastian Markbåge b6006201b5
Add a way to create Server Reference Proxies on the client (#26632)
This lets the client bundle encode Server References without them first
being passed from an RSC payload. Like if you just import `"use server"`
from the client. A bundler could already emit these proxies to be called
on the client but the subtle difference is that those proxies couldn't
be passed back into the server by reference. They have to be registered
with React.

We don't currently implement importing `"use server"` from client
components in the reference implementation. It'd need to expand the
Webpack plugin with a loader that rewrites files with the `"use server"`
in the client bundle.

```
"use server";

export async function action() {
   ...
}
```
->
```
import {createServerReference} from "react-server-dom-webpack/client";
import {callServer} from "some-router/call-server";

export const action = createServerReference('1234#action', callServer);
```

The technique I use here is that the compiled output has to call
`createServerReference(id, callServer)` with the `$$id` and proxy
implementation. We then return a proxy function that is registered with
a WeakMap to the particular instance of the Flight Client.

This might be hard to implement because it requires emitting module
imports to a specific stateful runtime module in the compiler. A benefit
is that this ensures that this particular reference is locked to a
specific client if there are multiple - e.g. talking to different
servers.

It's fairly arbitrary whether we use a WeakMap technique (like we do on
the client) vs an `$$id` (like we do on the server). Not sure what's
best overall. The WeakMap is nice because it doesn't leak implementation
details that might be abused to consumers. We should probably pick one
and unify.
2023-04-14 23:20:35 -04:00
Sebastian Markbåge da6c23a45c
[Flight] Fallback to importing the whole module instead of encoding every name (#26624)
We currently don't just "require" a module by its module id/path. We
encode the pair of module id/path AND its export name. That's because
with module splitting, a single original module can end up in two or
more separate modules by name. Therefore the manifest files need to
encode how to require the whole module as well as how to require each
export name.

In practice, we don't currently use this because we end up forcing
Webpack to deopt and keep it together as a single module, and we don't
even have the code in the Webpack plugin to write separate values for
each export name.

The problem is with CJS we don't statically know what all the export
names will be. Since these cases will never be module split, we don't
really need to know.

This changes the Flight requires to first look for the specific name
we're loading and then if that name doesn't exist in the manifest we
fallback to looking for the `"*"` name containing the entire module and
look for the name in there at runtime.

We could probably optimize this a bit if we assume that CJS modules on
the server never get built with a name. That way we don't have to do the
failed lookup.

Additionally, since we've recently merged filepath + name into a single
string instead of two values, we now have to split those back out by
parsing the string. This is especially unfortunate for server references
since those should really not reveal what they are but be a hash or
something. The solution might just be to split them back out into two
separate fields again.

cc @shuding
2023-04-14 21:09:09 -04:00
Sophie Alpert 2bfe4b246f
[Flight] Fix style nit from #26623 (#26629)
Maybe this is faster.
https://github.com/facebook/react/pull/26623#discussion_r1167053174
2023-04-14 09:49:41 -07:00
Sophie Alpert ab2385fa38
[Flight] Serialize weird numbers (#26623) 2023-04-14 09:28:48 -07:00
Rubén Norte 39a3b72c6d
Post-process build files for React Native to add generated signature and @nolint (#26616)
## Summary

We're enabling a new mechanism to synchronize build files from `react`
to `react-native`. That new mechanism doesn't post-process files, so we
need to add that post-processing somewhere. This PR does that when
generating the files in the first place, so the generated files in the
`build` directory are ready to be committed in the `react-native`
repository directly.

This makes use of `signedsource` to avoid direct modifications of these
files in the `react-native` repository, as well as `@noformat` and
`@nolint` to prevent unactionable CI failures in that repository.

## How did you test this change?

Generated build files for `react-native` before and after this change:
```
node ./scripts/rollup/build-all-release-channels.js react-native
```

Checked new contents. Relevant changes:

```diff
diff --color -r build-before/react-native/implementations/ReactFabric-dev.fb.js build-after/react-native/implementations/ReactFabric-dev.fb.js
10c10
<  * @generated
---
>  * @generated SignedSource<<03cef14e77b8250b567dfdf3b066085e>>
diff --color -r build-before/react-native/implementations/ReactFabric-dev.js build-after/react-native/implementations/ReactFabric-dev.js
11c11
<  * @generated
---
>  * @generated SignedSource<<e39eed38a363846ca9ee9b59a225683c>>
diff --color -r build-before/react-native/implementations/ReactFabric-prod.fb.js build-after/react-native/implementations/ReactFabric-prod.fb.js
10c10
<  * @generated
---
>  * @generated SignedSource<<f65efcd6a469d5f6fef1ce647e5ec09a>>
diff --color -r build-before/react-native/implementations/ReactFabric-prod.js build-after/react-native/implementations/ReactFabric-prod.js
11c11
<  * @generated
---
>  * @generated SignedSource<<cdd582aa889b1054b2c5faf412622b18>>
diff --color -r build-before/react-native/implementations/ReactFabric-profiling.fb.js build-after/react-native/implementations/ReactFabric-profiling.fb.js
10c10
<  * @generated
---
>  * @generated SignedSource<<81e74849b24f104882bd298f062be0fa>>
diff --color -r build-before/react-native/implementations/ReactFabric-profiling.js build-after/react-native/implementations/ReactFabric-profiling.js
11c11
<  * @generated
---
>  * @generated SignedSource<<c050b7fa1453dc21ac1c5b98146210a8>>
diff --color -r build-before/react-native/implementations/ReactNativeRenderer-dev.fb.js build-after/react-native/implementations/ReactNativeRenderer-dev.fb.js
10c10
<  * @generated
---
>  * @generated SignedSource<<9c03464b489b41c06a065aeba8619263>>
diff --color -r build-before/react-native/implementations/ReactNativeRenderer-dev.js build-after/react-native/implementations/ReactNativeRenderer-dev.js
11c11
<  * @generated
---
>  * @generated SignedSource<<18b34c037544949dcf9b28f945921ba8>>
diff --color -r build-before/react-native/implementations/ReactNativeRenderer-prod.fb.js build-after/react-native/implementations/ReactNativeRenderer-prod.fb.js
10c10
<  * @generated
---
>  * @generated SignedSource<<592e9654c584d1da523378b119bd8bd7>>
diff --color -r build-before/react-native/implementations/ReactNativeRenderer-prod.js build-after/react-native/implementations/ReactNativeRenderer-prod.js
11c11
<  * @generated
---
>  * @generated SignedSource<<91c894db99e2d76f8a32708ad6ad1bde>>
diff --color -r build-before/react-native/implementations/ReactNativeRenderer-profiling.fb.js build-after/react-native/implementations/ReactNativeRenderer-profiling.fb.js
10c10
<  * @generated
---
>  * @generated SignedSource<<5ce378a9216ea747d91b208b9fd1ebd5>>
diff --color -r build-before/react-native/implementations/ReactNativeRenderer-profiling.js build-after/react-native/implementations/ReactNativeRenderer-profiling.js
11c11
<  * @generated
---
>  * @generated SignedSource<<1c7564f446ee83142976035b2884dcfd>>
diff --color -r build-before/react-native/shims/ReactFabric.js build-after/react-native/shims/ReactFabric.js
7c7
<  * @format
---
>  * @noformat
8a9,10
>  * @nolint
>  * @generated SignedSource<<cece19ddbec9f287c995721f49c68977>>
diff --color -r build-before/react-native/shims/ReactFeatureFlags.js build-after/react-native/shims/ReactFeatureFlags.js
7c7
<  * @format
---
>  * @noformat
8a9,10
>  * @nolint
>  * @generated SignedSource<<2881c8e89ef0f73f4cf6612cb518b197>>
diff --color -r build-before/react-native/shims/ReactNative.js build-after/react-native/shims/ReactNative.js
7c7
<  * @format
---
>  * @noformat
8a9,10
>  * @nolint
>  * @generated SignedSource<<0debd6e5a17dc037cb4661315a886de6>>
diff --color -r build-before/react-native/shims/ReactNativeTypes.js build-after/react-native/shims/ReactNativeTypes.js
7c7
<  * @format
---
>  * @noformat
8a9,10
>  * @nolint
>  * @generated SignedSource<<652b117c94307244bcf5e4af18928903>>
diff --color -r build-before/react-native/shims/ReactNativeViewConfigRegistry.js build-after/react-native/shims/ReactNativeViewConfigRegistry.js
7c7
<  * @format
---
>  * @noformat
8a9,10
>  * @nolint
>  * @generated SignedSource<<ce82e8957367bee7d11379ab88e3f7c5>>
diff --color -r build-before/react-native/shims/createReactNativeComponentClass.js build-after/react-native/shims/createReactNativeComponentClass.js
7c7
<  * @format
---
>  * @noformat
8a9,10
>  * @nolint
>  * @generated SignedSource<<ede54ac2fa1b9a09e234cdf098048989>>
```
2023-04-14 11:42:48 +02:00
Tianyu Yao d121c67004
Synchronously flush the transition lane scheduled in a popstate event (#26025)
<!--
  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 debug-test --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

Browsers restore state like forms and scroll position right after the
popstate event. To make sure the page work as expected on back or
forward button, we need to flush transitions scheduled in a popstate
synchronously, and only yields if it suspends.
This PR adds a new HostConfig method to check if `window.event ===
'popstate'`, and `scheduleMicrotask` if a transition is scheduled in a
`PopStateEvent`.

## How did you test this change?

yarn test
2023-04-13 15:21:19 -04:00
Samuel Susla 7b0642bb98
Remove revertRemovalOfSiblingPrerendering killswitch (#26549)
removal of sibling prerendering has been rolled out at Meta. We can
delete the flag now.
2023-04-12 21:05:17 -04:00
Andrew Clark 8256781fdf
Throttle retries even if everything has loaded (#26611)
If a Suspense fallback is shown, and the data finishes loading really
quickly after that, we throttle the content from appearing for 500ms to
reduce thrash.

This already works for successive fallback states (like if one fallback
is nested inside another) but it wasn't being applied to the final step
in the sequence: if there were no more unresolved Suspense boundaries in
the tree, the content would appear immediately.

This fixes the throttling behavior so that it applies to all renders
that are the result of suspended data being loaded. (Our internal jargon
term for this is a "retry".)
2023-04-12 20:25:32 -04:00
316 changed files with 11409 additions and 5993 deletions

View File

@ -426,7 +426,6 @@ jobs:
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
workflows:
version: 2
build_and_test:
unless: << pipeline.parameters.prerelease_commit_sha >>
@ -605,10 +604,20 @@ workflows:
when: << pipeline.parameters.prerelease_commit_sha >>
jobs:
- publish_prerelease:
name: Publish to Next channel
name: Publish to Canary channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: stable
dist_tag: "next"
# The tags to use when publishing canaries. The main one we should
# always include is "canary" but we can use multiple (e.g. alpha,
# beta, rc). To declare multiple, use a comma-separated string, like
# this:
# dist_tag: "canary,alpha,beta,rc"
#
# TODO: We currently tag canaries with "next" in addition to "canary"
# because this used to be called the "next" channel and some
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: "canary,next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -616,7 +625,7 @@ workflows:
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Next channel
- Publish to Canary channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: experimental
dist_tag: experimental
@ -634,10 +643,10 @@ workflows:
- main
jobs:
- publish_prerelease:
name: Publish to Next channel
name: Publish to Canary channel
commit_sha: << pipeline.git.revision >>
release_channel: stable
dist_tag: "next"
dist_tag: "canary,next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -645,7 +654,7 @@ workflows:
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Next channel
- Publish to Canary channel
commit_sha: << pipeline.git.revision >>
release_channel: experimental
dist_tag: experimental

View File

@ -236,7 +236,14 @@ module.exports = {
'no-inner-declarations': [ERROR, 'functions'],
'no-multi-spaces': ERROR,
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
'no-restricted-syntax': [ERROR, 'WithStatement'],
'no-restricted-syntax': [
ERROR,
'WithStatement',
{
selector: 'MemberExpression[property.name=/^(?:substring|substr)$/]',
message: 'Prefer string.slice() over .substring() and .substr().',
},
],
'no-shadow': ERROR,
'no-unused-vars': [ERROR, {args: 'none'}],
'no-use-before-define': OFF,
@ -409,7 +416,6 @@ module.exports = {
{
files: [
'packages/react-native-renderer/**/*.js',
'packages/react-server-native-relay/**/*.js',
],
globals: {
nativeFabricUIManager: 'readonly',

View File

@ -1,13 +1,22 @@
name: Commit Artifacts for Facebook WWW and fbsource
name: Commit Artifacts for Meta WWW and fbsource
on:
push:
branches: [main]
branches: [main, meta-www, meta-fbsource]
jobs:
download_artifacts:
runs-on: ubuntu-latest
outputs:
www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
steps:
- uses: actions/checkout@v3
- name: "Check branches"
id: check_branches
run: |
echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
- name: Download and unzip artifacts
uses: actions/github-script@v6
env:
@ -168,6 +177,7 @@ jobs:
commit_www_artifacts:
needs: download_artifacts
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -204,6 +214,7 @@ jobs:
commit_fbsource_artifacts:
needs: download_artifacts
runs-on: ubuntu-latest
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
steps:
- uses: actions/checkout@v3
with:
@ -217,7 +228,16 @@ jobs:
name: compiled-rn
path: compiled-rn/
- run: git status -u
- name: Check if only the REVISION file has changed
id: check_should_commit
run: |
if git status --porcelain | grep -qv '/REVISION$'; then
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Commit changes to branch
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: |

View File

@ -1,4 +1,4 @@
# [React](https://reactjs.org/) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/facebook/react) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
# [React](https://reactjs.org/) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield)](https://circleci.com/gh/facebook/react) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
React is a JavaScript library for building user interfaces.

View File

@ -7,12 +7,12 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 18.0.0
// 18.3.0
//
// The @next channel appends additional information, with the scheme
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 18.0.0-alpha-a1c2d3e4
// 18.3.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
@ -20,9 +20,13 @@
const ReactVersion = '18.3.0';
// The label used by the @next channel. Represents the upcoming release's
// stability. Could be "alpha", "beta", "rc", etc.
const nextChannelLabel = 'next';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
// choose to change it to "alpha", "beta", "rc", etc.
//
// It only affects the label used in the version string. To customize the
// npm dist tags used during publish, refer to .circleci/config.yml.
const canaryChannelLabel = 'canary';
const stablePackages = {
'eslint-plugin-react-hooks': '5.0.0',
@ -40,14 +44,14 @@ const stablePackages = {
scheduler: '0.24.0',
};
// These packages do not exist in the @next or @latest channel, only
// These packages do not exist in the @canary or @latest channel, only
// @experimental. We don't use semver, just the commit sha, so this is just a
// list of package names instead of a map.
const experimentalPackages = [];
module.exports = {
ReactVersion,
nextChannelLabel,
canaryChannelLabel,
stablePackages,
experimentalPackages,
};

View File

@ -22,7 +22,7 @@ class App extends PureComponent {
}
const multiplier = input.length !== 0 ? input.length : 1;
const complexity =
(parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
(parseInt(window.location.search.slice(1), 10) / 100) * 25 || 25;
const data = _.range(5).map(t =>
_.range(complexity * multiplier).map((j, i) => {
return {

View File

@ -15,8 +15,8 @@
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
@ -38,4 +38,4 @@
Learn more at https://reactjs.org/docs/getting-started.html
-->
</body>
</html>
</html>

View File

@ -11,12 +11,12 @@
__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
</script>
<script src="https://unpkg.com/scheduler@next/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/scheduler@next/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@next/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@next/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/scheduler@canary/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
@ -38,4 +38,4 @@
Learn more at https://reactjs.org/docs/getting-started.html
-->
</body>
</html>
</html>

View File

@ -11,7 +11,7 @@ import semver from 'semver';
function parseQuery(qstr) {
var query = {};
var a = qstr.substr(1).split('&');
var a = qstr.slice(1).split('&');
for (var i = 0; i < a.length; i++) {
var b = a[i].split('=');

View File

@ -95,6 +95,8 @@ app.all('/', async function (req, res, next) {
if (req.get('rsc-action')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
proxiedHeaders['rsc-action'] = req.get('rsc-action');
} else if (req.get('Content-type')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
}
const promiseForData = request(

View File

@ -36,6 +36,7 @@ const bodyParser = require('body-parser');
const busboy = require('busboy');
const app = express();
const compress = require('compression');
const {Readable} = require('node:stream');
app.use(compress());
@ -45,7 +46,7 @@ const {readFile} = require('fs').promises;
const React = require('react');
app.get('/', async function (req, res) {
async function renderApp(res, returnValue) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@ -91,37 +92,74 @@ app.get('/', async function (req, res) {
),
React.createElement(App),
];
const {pipe} = renderToPipeableStream(root, moduleMap);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = returnValue ? {returnValue, root} : root;
const {pipe} = renderToPipeableStream(payload, moduleMap);
pipe(res);
}
app.get('/', async function (req, res) {
await renderApp(res, null);
});
app.post('/', bodyParser.text(), async function (req, res) {
const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
await import('react-server-dom-webpack/server');
const {
renderToPipeableStream,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
} = await import('react-server-dom-webpack/server');
const serverReference = req.get('rsc-action');
const [filepath, name] = serverReference.split('#');
const action = (await import(filepath))[name];
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action');
}
if (serverReference) {
// This is the client-side case
const [filepath, name] = serverReference.split('#');
const action = (await import(filepath))[name];
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action');
}
let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
req.pipe(bb);
args = await reply;
let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
req.pipe(bb);
args = await reply;
} else {
args = await decodeReply(req.body);
}
const result = action.apply(null, args);
try {
// Wait for any mutations
await result;
} catch (x) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result);
} else {
args = await decodeReply(req.body);
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
const fakeRequest = new UndiciRequest('http://localhost', {
method: 'POST',
headers: {'Content-Type': req.headers['content-type']},
body: Readable.toWeb(req),
duplex: 'half',
});
const formData = await fakeRequest.formData();
const action = await decodeAction(formData);
try {
// Wait for any mutations
await action();
} catch (x) {
const {setServerState} = await import('../src/ServerState.js');
setServerState('Error: ' + x.message);
}
renderApp(res, null);
}
const result = action.apply(null, args);
const {pipe} = renderToPipeableStream(result, {});
pipe(res);
});
app.get('/todos', function (req, res) {

View File

@ -7,8 +7,11 @@ import {Counter as Counter2} from './Counter2.js';
import ShowMore from './ShowMore.js';
import Button from './Button.js';
import Form from './Form.js';
import {like} from './actions.js';
import {like, greet} from './actions.js';
import {getServerState} from './ServerState.js';
export default async function App() {
const res = await fetch('http://localhost:3001/todos');
@ -22,7 +25,7 @@ export default async function App() {
</head>
<body>
<Container>
<h1>Hello, world</h1>
<h1>{getServerState()}</h1>
<Counter />
<Counter2 />
<ul>
@ -33,6 +36,7 @@ export default async function App() {
<ShowMore>
<p>Lorem ipsum</p>
</ShowMore>
<Form action={greet} />
<div>
<Button action={like}>Like</Button>
</div>

View File

@ -1,25 +1,26 @@
'use client';
import * as React from 'react';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
export default function Button({action, children}) {
const [isPending, setIsPending] = React.useState(false);
function ButtonDisabledWhilePending({action, children}) {
const {pending} = useFormStatus();
return (
<button
disabled={isPending}
onClick={async () => {
setIsPending(true);
try {
const result = await action();
console.log(result);
} catch (error) {
console.error(error);
} finally {
setIsPending(false);
}
}}>
<button disabled={pending} formAction={action}>
{children}
</button>
);
}
export default function Button({action, children}) {
return (
<ErrorBoundary>
<form>
<ButtonDisabledWhilePending action={action}>
{children}
</ButtonDisabledWhilePending>
</form>
</ErrorBoundary>
);
}

View File

@ -0,0 +1,16 @@
'use client';
import * as React from 'react';
export default class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <div>Caught an error: {this.state.error.message}</div>;
}
return this.props.children;
}
}

View File

@ -0,0 +1,29 @@
'use client';
import * as React from 'react';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
function Status() {
const {pending} = useFormStatus();
return pending ? 'Saving...' : null;
}
export default function Form({action, children}) {
const [isPending, setIsPending] = React.useState(false);
return (
<ErrorBoundary>
<form action={action}>
<label>
Name: <input name="name" />
</label>
<label>
File: <input type="file" name="file" />
</label>
<button>Say Hi</button>
<Status />
</form>
</ErrorBoundary>
);
}

View File

@ -0,0 +1,9 @@
let serverState = 'Hello World';
export function setServerState(message) {
serverState = message;
}
export function getServerState() {
return serverState;
}

View File

@ -1,5 +1,20 @@
'use server';
import {setServerState} from './ServerState.js';
export async function like() {
setServerState('Liked!');
return new Promise((resolve, reject) => resolve('Liked'));
}
export async function greet(formData) {
const name = formData.get('name') || 'you';
setServerState('Hi ' + name);
const file = formData.get('file');
if (file) {
return `Ok, ${name}, here is ${file.name}:
${(await file.text()).toUpperCase()}
`;
}
return 'Hi ' + name + '!';
}

View File

@ -1,11 +1,29 @@
import * as React from 'react';
import {Suspense} from 'react';
import {use, Suspense, useState, startTransition} from 'react';
import ReactDOM from 'react-dom/client';
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
// TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
import './style.css';
let updateRoot;
async function callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
const {returnValue, root} = await createFromFetch(response, {callServer});
// Refresh the tree with the new RSC payload.
startTransition(() => {
updateRoot(root);
});
return returnValue;
}
let data = createFromFetch(
fetch('/', {
headers: {
@ -13,21 +31,14 @@ let data = createFromFetch(
},
}),
{
async callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
return createFromFetch(response);
},
callServer,
}
);
// TODO: This transition shouldn't really be necessary but it is for now.
React.startTransition(() => {
ReactDOM.hydrateRoot(document, data);
});
function Shell({data}) {
const [root, setRoot] = useState(use(data));
updateRoot = setRoot;
return root;
}
ReactDOM.hydrateRoot(document, <Shell data={data} />);

View File

@ -65,8 +65,8 @@
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "^3.0.1",
"filesize": "^6.0.1",
"flow-bin": "^0.202.0",
"flow-remove-types": "^2.202.0",
"flow-bin": "^0.205.1",
"flow-remove-types": "^2.205.1",
"glob": "^7.1.6",
"glob-stream": "^6.1.0",
"google-closure-compiler": "^20230206.0.0",
@ -82,7 +82,6 @@
"minimist": "^1.2.3",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"pacote": "^10.3.0",
"prettier": "2.8.3",
"pretty-format": "^29.4.1",
"prop-types": "^15.6.2",
@ -93,6 +92,7 @@
"rollup-plugin-prettier": "^3.0.0",
"rollup-plugin-strip-banner": "^3.0.0",
"semver": "^7.1.1",
"signedsource": "^2.0.0",
"targz": "^1.0.1",
"through2": "^3.0.1",
"tmp": "^0.1.0",
@ -102,7 +102,7 @@
"yargs": "^15.3.1"
},
"devEngines": {
"node": "16.x || 18.x || 19.x"
"node": "16.x || 18.x || 19.x || 20.x"
},
"jest": {
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
@ -117,7 +117,7 @@
"lint": "node ./scripts/tasks/eslint.js",
"lint-build": "node ./scripts/rollup/validate/index.js",
"extract-errors": "node scripts/error-codes/extract-errors.js",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js",
"debug-test": "yarn test --deprecated 'yarn test --debug'",
"test": "node ./scripts/jest/jest-cli.js",
"test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable",

View File

@ -18,7 +18,7 @@ const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps'];
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
// ***************************************************

View File

@ -26,7 +26,7 @@ ESLintTester.setDefaultConfig({
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
// ***************************************************

View File

@ -1103,7 +1103,7 @@ export default {
extraWarning =
` You can also do a functional update '${
setStateRecommendation.setter
}(${setStateRecommendation.missingDep.substring(
}(${setStateRecommendation.missingDep.slice(
0,
1,
)} => ...)' if you only need '${

View File

@ -479,11 +479,5 @@ export function suspendInstance(type, props) {}
export function waitForCommitToBeReady() {
return null;
}
// eslint-disable-next-line no-undef
export function prepareRendererToRender(container: Container): void {
// noop
}
export function resetRendererAfterRender(): void {
// noop
}
export const NotPendingTransition = null;

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightClientStream';
export * from './src/ReactFlightClient';

View File

@ -13,25 +13,37 @@ import type {LazyComponent} from 'react/src/ReactLazy';
import type {
ClientReference,
ClientReferenceMetadata,
UninitializedModel,
Response,
SSRManifest,
StringDecoder,
} from './ReactFlightClientConfig';
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
import type {CallServerCallback} from './ReactFlightReplyClient';
import {
resolveClientReference,
preloadModule,
requireModule,
parseModel,
dispatchHint,
readPartialStringChunk,
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
} from './ReactFlightClientConfig';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import {
encodeFormAction,
knownServerReferences,
} from './ReactFlightReplyClient';
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type {CallServerCallback};
type UninitializedModel = string;
export type JSONValue =
| number
@ -150,15 +162,15 @@ Chunk.prototype.then = function <T>(
}
};
export type ResponseBase = {
export type Response = {
_bundlerConfig: SSRManifest,
_callServer: CallServerCallback,
_chunks: Map<number, SomeChunk<any>>,
...
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
};
export type {Response};
function readChunk<T>(chunk: SomeChunk<T>): T {
// If we have resolved content, we try to initialize it first which
// might put us back into one of the other states.
@ -497,11 +509,14 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
return callServer(metaData.id, bound.concat(args));
});
};
// Expose encoder for use by SSR.
// TODO: Only expose this in SSR builds and not the browser client.
proxy.$$FORM_ACTION = encodeFormAction;
knownServerReferences.set(proxy, metaData);
return proxy;
}
export function parseModelString(
function parseModelString(
response: Response,
parentObject: Object,
key: string,
@ -515,11 +530,11 @@ export function parseModelString(
switch (value[1]) {
case '$': {
// This was an escaped string value.
return value.substring(1);
return value.slice(1);
}
case 'L': {
// Lazy node
const id = parseInt(value.substring(2), 16);
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
@ -527,21 +542,21 @@ export function parseModelString(
}
case '@': {
// Promise
const id = parseInt(value.substring(2), 16);
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
case 'S': {
// Symbol
return Symbol.for(value.substring(2));
return Symbol.for(value.slice(2));
}
case 'P': {
// Server Context Provider
return getOrCreateServerContext(value.substring(2)).Provider;
return getOrCreateServerContext(value.slice(2)).Provider;
}
case 'F': {
// Server Reference
const id = parseInt(value.substring(2), 16);
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
@ -559,18 +574,38 @@ export function parseModelString(
throw chunk.reason;
}
}
case 'I': {
// $Infinity
return Infinity;
}
case '-': {
// $-0 or $-Infinity
if (value === '$-0') {
return -0;
} else {
return -Infinity;
}
}
case 'N': {
// $NaN
return NaN;
}
case 'u': {
// matches "$undefined"
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
return undefined;
}
case 'D': {
// Date
return new Date(Date.parse(value.slice(2)));
}
case 'n': {
// BigInt
return BigInt(value.substring(2));
return BigInt(value.slice(2));
}
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.substring(1), 16);
const id = parseInt(value.slice(1), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
@ -601,7 +636,7 @@ export function parseModelString(
return value;
}
export function parseModelTuple(
function parseModelTuple(
response: Response,
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
): any {
@ -625,17 +660,25 @@ function missingCall() {
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): ResponseBase {
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response = {
const response: Response = {
_bundlerConfig: bundlerConfig,
_callServer: callServer !== undefined ? callServer : missingCall,
_chunks: chunks,
_partialRow: '',
_stringDecoder: (null: any),
_fromJSON: (null: any),
};
if (supportsBinaryStreams) {
response._stringDecoder = createStringDecoder();
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
}
export function resolveModel(
function resolveModel(
response: Response,
id: number,
model: UninitializedModel,
@ -649,7 +692,7 @@ export function resolveModel(
}
}
export function resolveModule(
function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
@ -698,7 +741,7 @@ export function resolveModule(
}
type ErrorWithDigest = Error & {digest?: string};
export function resolveErrorProd(
function resolveErrorProd(
response: Response,
id: number,
digest: string,
@ -727,7 +770,7 @@ export function resolveErrorProd(
}
}
export function resolveErrorDev(
function resolveErrorDev(
response: Response,
id: number,
digest: string,
@ -758,6 +801,114 @@ export function resolveErrorDev(
}
}
function resolveHint(
response: Response,
code: string,
model: UninitializedModel,
): void {
const hintModel = parseModel<HintModel>(response, model);
dispatchHint(code, hintModel);
}
function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const colon = row.indexOf(':', 0);
const id = parseInt(row.slice(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
switch (tag) {
case 'I': {
resolveModule(response, id, row.slice(colon + 2));
return;
}
case 'H': {
const code = row[colon + 2];
resolveHint(response, code, row.slice(colon + 3));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
}
return;
}
default: {
// We assume anything else is JSON.
resolveModel(response, id, row.slice(colon + 1));
return;
}
}
}
export function processStringChunk(
response: Response,
chunk: string,
offset: number,
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
const fullrow = response._partialRow + chunk.slice(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
response._partialRow += chunk.slice(offset);
}
export function processBinaryChunk(
response: Response,
chunk: Uint8Array,
): void {
if (!supportsBinaryStreams) {
throw new Error("This environment don't support binary chunks.");
}
const stringDecoder = response._stringDecoder;
let linebreak = chunk.indexOf(10); // newline
while (linebreak > -1) {
const fullrow =
response._partialRow +
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
processFullRow(response, fullrow);
response._partialRow = '';
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10); // newline
}
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
}
function parseModel<T>(response: Response, json: UninitializedModel): T {
return JSON.parse(json, response._fromJSON);
}
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
}
if (typeof value === 'object' && value !== null) {
return parseModelTuple(response, value);
}
return value;
};
}
export function close(response: Response): void {
// In case there are any remaining unresolved chunks, they won't
// be resolved now. So we need to issue an error to those.

View File

@ -1,33 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type StringDecoder = void;
export const supportsBinaryStreams = false;
export function createStringDecoder(): void {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}
export function readPartialStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}
export function readFinalStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ResponseBase} from './ReactFlightClient';
import type {StringDecoder} from './ReactFlightClientConfig';
export type Response = ResponseBase & {
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
};
export type UninitializedModel = string;
export function parseModel<T>(response: Response, json: UninitializedModel): T {
return JSON.parse(json, response._fromJSON);
}

View File

@ -1,140 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {CallServerCallback} from './ReactFlightClient';
import type {Response} from './ReactFlightClientConfigStream';
import type {SSRManifest} from './ReactFlightClientConfig';
import {
resolveModule,
resolveModel,
resolveErrorProd,
resolveErrorDev,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
} from './ReactFlightClient';
import {
readPartialStringChunk,
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
} from './ReactFlightClientConfig';
export type {Response};
function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const colon = row.indexOf(':', 0);
const id = parseInt(row.substring(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
switch (tag) {
case 'I': {
resolveModule(response, id, row.substring(colon + 2));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.substring(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
}
return;
}
default: {
// We assume anything else is JSON.
resolveModel(response, id, row.substring(colon + 1));
return;
}
}
}
export function processStringChunk(
response: Response,
chunk: string,
offset: number,
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
const fullrow = response._partialRow + chunk.substring(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
response._partialRow += chunk.substring(offset);
}
export function processBinaryChunk(
response: Response,
chunk: Uint8Array,
): void {
if (!supportsBinaryStreams) {
throw new Error("This environment don't support binary chunks.");
}
const stringDecoder = response._stringDecoder;
let linebreak = chunk.indexOf(10); // newline
while (linebreak > -1) {
const fullrow =
response._partialRow +
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
processFullRow(response, fullrow);
response._partialRow = '';
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10); // newline
}
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
}
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
}
if (typeof value === 'object' && value !== null) {
return parseModelTuple(response, value);
}
return value;
};
}
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): Response {
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
// It should be inlined to one object literal but minor changes can break it.
const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
const response: any = createResponseBase(bundlerConfig, callServer);
response._partialRow = '';
if (supportsBinaryStreams) {
response._stringDecoder = stringDecoder;
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
}
export {reportGlobalError, getRoot, close} from './ReactFlightClient';

View File

@ -7,9 +7,7 @@
* @flow
*/
import type {Thenable} from 'shared/ReactTypes';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
import {
REACT_ELEMENT_TYPE,
@ -25,6 +23,10 @@ import {
} from 'shared/ReactSerializationErrors';
import isArray from 'shared/isArray';
import type {
FulfilledThenable,
RejectedThenable,
} from '../../shared/ReactTypes';
type ReactJSONValue =
| string
@ -36,6 +38,15 @@ type ReactJSONValue =
export opaque type ServerReference<T> = T;
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type ServerReferenceId = any;
export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();
// Serializable values
export type ReactServerValue =
// References are passed by their value
@ -71,10 +82,39 @@ function serializeSymbolReference(name: string): string {
return '$S' + name;
}
function serializeFormDataReference(id: number): string {
// Why K? F is "Function". D is "Date". What else?
return '$K' + id.toString(16);
}
function serializeNumber(number: number): string | number {
if (Number.isFinite(number)) {
if (number === 0 && 1 / number === -Infinity) {
return '$-0';
} else {
return number;
}
} else {
if (number === Infinity) {
return '$Infinity';
} else if (number === -Infinity) {
return '$-Infinity';
} else {
return '$NaN';
}
}
}
function serializeUndefined(): string {
return '$undefined';
}
function serializeDateFromDateJSON(dateJSON: string): string {
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
// We need only tack on a $D prefix.
return '$D' + dateJSON;
}
function serializeBigInt(n: bigint): string {
return '$n' + n.toString(10);
}
@ -91,6 +131,7 @@ function escapeStringValue(value: string): string {
export function processReply(
root: ReactServerValue,
formFieldPrefix: string,
resolve: (string | FormData) => void,
reject: (error: mixed) => void,
): void {
@ -106,10 +147,16 @@ export function processReply(
value: ReactServerValue,
): ReactJSONValue {
const parent = this;
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
if (__DEV__) {
// $FlowFixMe[incompatible-use]
const originalValue = this[key];
if (typeof originalValue === 'object' && originalValue !== value) {
const originalValue = parent[key];
if (
typeof originalValue === 'object' &&
originalValue !== value &&
!(originalValue instanceof Date)
) {
if (objectName(originalValue) !== 'Object') {
console.error(
'Only plain objects can be passed to Server Functions from the Client. ' +
@ -150,7 +197,7 @@ export function processReply(
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
const data: FormData = formData;
// eslint-disable-next-line react-internal/safe-string-coercion
data.append('' + promiseId, partJSON);
data.append(formFieldPrefix + promiseId, partJSON);
pendingParts--;
if (pendingParts === 0) {
resolve(data);
@ -164,6 +211,24 @@ export function processReply(
);
return serializePromiseID(promiseId);
}
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
if (value instanceof FormData) {
if (formData === null) {
// Upgrade to use FormData to allow us to use rich objects as its values.
formData = new FormData();
}
const data: FormData = formData;
const refId = nextPartId++;
// Copy all the form fields with a prefix for this reference.
// These must come first in the form order because we assume that all the
// fields are available before this is referenced.
const prefix = formFieldPrefix + refId + '_';
// $FlowFixMe[prop-missing]: FormData has forEach.
value.forEach((originalValue: string | File, originalKey: string) => {
data.append(prefix + originalKey, originalValue);
});
return serializeFormDataReference(refId);
}
if (!isArray(value)) {
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
@ -221,13 +286,27 @@ export function processReply(
}
if (typeof value === 'string') {
// TODO: Maybe too clever. If we support URL there's no similar trick.
if (value[value.length - 1] === 'Z') {
// Possibly a Date, whose toJSON automatically calls toISOString
// $FlowFixMe[incompatible-use]
const originalValue = parent[key];
if (originalValue instanceof Date) {
return serializeDateFromDateJSON(value);
}
}
return escapeStringValue(value);
}
if (typeof value === 'boolean' || typeof value === 'number') {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return serializeNumber(value);
}
if (typeof value === 'undefined') {
return serializeUndefined();
}
@ -243,7 +322,7 @@ export function processReply(
// The reference to this function came from the same client so we can pass it back.
const refId = nextPartId++;
// eslint-disable-next-line react-internal/safe-string-coercion
formData.set('' + refId, metaDataJSON);
formData.set(formFieldPrefix + refId, metaDataJSON);
return serializeServerReferenceID(refId);
}
throw new Error(
@ -283,10 +362,112 @@ export function processReply(
resolve(json);
} else {
// Otherwise, we use FormData to let us stream in the result.
formData.set('0', json);
formData.set(formFieldPrefix + '0', json);
if (pendingParts === 0) {
// $FlowFixMe[incompatible-call] this has already been refined.
resolve(formData);
}
}
}
const boundCache: WeakMap<
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
Thenable<FormData>,
> = new WeakMap();
function encodeFormData(reference: any): Thenable<FormData> {
let resolve, reject;
// We need to have a handle on the thenable so that we can synchronously set
// its status from processReply, when it can complete synchronously.
const thenable: Thenable<FormData> = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
processReply(
reference,
'',
(body: string | FormData) => {
if (typeof body === 'string') {
const data = new FormData();
data.append('0', body);
body = data;
}
const fulfilled: FulfilledThenable<FormData> = (thenable: any);
fulfilled.status = 'fulfilled';
fulfilled.value = body;
resolve(body);
},
e => {
const rejected: RejectedThenable<FormData> = (thenable: any);
rejected.status = 'rejected';
rejected.reason = e;
reject(e);
},
);
return thenable;
}
export function encodeFormAction(
this: any => Promise<any>,
identifierPrefix: string,
): ReactCustomFormAction {
const reference = knownServerReferences.get(this);
if (!reference) {
throw new Error(
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
'This is a bug in React.',
);
}
let data: null | FormData = null;
let name;
const boundPromise = reference.bound;
if (boundPromise !== null) {
let thenable = boundCache.get(reference);
if (!thenable) {
thenable = encodeFormData(reference);
boundCache.set(reference, thenable);
}
if (thenable.status === 'rejected') {
throw thenable.reason;
} else if (thenable.status !== 'fulfilled') {
throw thenable;
}
const encodedFormData = thenable.value;
// This is hacky but we need the identifier prefix to be added to
// all fields but the suspense cache would break since we might get
// a new identifier each time. So we just append it at the end instead.
const prefixedData = new FormData();
// $FlowFixMe[prop-missing]
encodedFormData.forEach((value: string | File, key: string) => {
prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
});
data = prefixedData;
// We encode the name of the prefix containing the data.
name = '$ACTION_REF_' + identifierPrefix;
} else {
// This is the simple case so we can just encode the ID.
name = '$ACTION_ID_' + reference.id;
}
return {
name: name,
method: 'POST',
encType: 'multipart/form-data',
data: data,
};
}
export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
): (...A) => Promise<T> {
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
// Expose encoder for use by SSR.
// TODO: Only expose this in SSR builds and not the browser client.
proxy.$$FORM_ACTION = encodeFormAction;
knownServerReferences.set(proxy, {id: id, bound: null});
return proxy;
}

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Thenable} from 'shared/ReactTypes';
type ServerReferenceId = any;
export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();

View File

@ -263,6 +263,30 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('can transport weird numbers', async () => {
const nums = [0, -0, Infinity, -Infinity, NaN];
function ComponentClient({prop}) {
expect(prop).not.toBe(nums);
expect(prop).toEqual(nums);
expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
return `prop: ${prop}`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={nums} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
// already checked -0 with expects above
'prop: 0,0,Infinity,-Infinity,NaN',
);
});
it('can transport BigInt', async () => {
function ComponentClient({prop}) {
return `prop: ${prop} (${typeof prop})`;
@ -282,6 +306,23 @@ describe('ReactFlight', () => {
);
});
it('can transport Date', async () => {
function ComponentClient({prop}) {
return `prop: ${prop.toISOString()}`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={new Date(1234567890123)} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
});
it('can render a lazy component as a shared component on the server', async () => {
function SharedComponent({text}) {
return (
@ -489,7 +530,6 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
});
// @gate enableUseHook
it('should error if a non-serializable value is passed to a host component', async () => {
function ClientImpl({children}) {
return children;
@ -600,7 +640,6 @@ describe('ReactFlight', () => {
});
});
// @gate enableUseHook
it('should trigger the inner most error boundary inside a Client Component', async () => {
function ServerComponent() {
throw new Error('This was thrown in the Server Component.');
@ -651,28 +690,39 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
const obj = {
toJSON() {
return 123;
},
};
expect(() => {
const transport = ReactNoopFlightServer.render(
<input value={new Date()} />,
);
const transport = ReactNoopFlightServer.render(<input value={obj} />);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Date objects are not supported.',
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <input value={{toJSON: function}}>\n' +
' ^^^^^^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
class MyError extends Error {
toJSON() {
return 123;
}
}
expect(() => {
const transport = ReactNoopFlightServer.render(
<div>Current date: {new Date()}</div>,
<div>Womp womp: {new MyError('spaghetti')}</div>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Current date: {Date}</div>\n' +
' ^^^^^^',
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^',
{withoutStack: true},
);
});
@ -704,37 +754,46 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
const obj = {
toJSON() {
return 123;
},
};
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client value={new Date()} />,
);
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Date objects are not supported.',
'Objects with toJSON methods are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
const obj = {
toJSON() {
return 123;
},
};
function ClientImpl({children}) {
return <div>{children}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client>Current date: {new Date()}</Client>,
<Client>Current date: {obj}</Client>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Date objects are not supported.\n' +
' <>Current date: {Date}</>\n' +
' ^^^^^^',
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <>Current date: {{toJSON: function}}</>\n' +
' ^^^^^^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});

View File

@ -25,7 +25,6 @@
declare var $$$config: any;
export type Response = any;
export opaque type SSRManifest = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
@ -35,12 +34,10 @@ export const resolveClientReference = $$$config.resolveClientReference;
export const resolveServerReference = $$$config.resolveServerReference;
export const preloadModule = $$$config.preloadModule;
export const requireModule = $$$config.requireModule;
export const dispatchHint = $$$config.dispatchHint;
export opaque type Source = mixed;
export type UninitializedModel = string;
export const parseModel = $$$config.parseModel;
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
export const supportsBinaryStreams = $$$config.supportsBinaryStreams;

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,7 +8,6 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export type Response = any;

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from 'react-server-dom-relay/src/ReactFlightClientConfigDOMRelay';
export * from '../ReactFlightClientConfigNoStream';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from 'react-server-native-relay/src/ReactFlightClientConfigNativeRelay';
export * from '../ReactFlightClientConfigNoStream';

View File

@ -51,9 +51,19 @@ type Dispatch<A> = A => void;
let primitiveStackCache: null | Map<string, Array<any>> = null;
type MemoCache = {
data: Array<Array<any>>,
index: number,
};
type FunctionComponentUpdateQueue = {
memoCache?: MemoCache | null,
};
type Hook = {
memoizedState: any,
next: Hook | null,
updateQueue: FunctionComponentUpdateQueue | null,
};
function getPrimitiveStackCache(): Map<string, Array<any>> {
@ -79,6 +89,10 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useDebugValue(null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
if (typeof Dispatcher.useMemoCache === 'function') {
// This type check is for Flow only.
Dispatcher.useMemoCache(0);
}
} finally {
readHookLog = hookLog;
hookLog = [];
@ -106,6 +120,13 @@ function readContext<T>(context: ReactContext<T>): T {
return context._currentValue;
}
function use<T>(): T {
// TODO: What should this do if it receives an unresolved promise?
throw new Error(
'Support for `use` not yet implemented in react-debug-tools.',
);
}
function useContext<T>(context: ReactContext<T>): T {
hookLog.push({
primitive: 'Context',
@ -326,7 +347,40 @@ function useId(): string {
return id;
}
function useMemoCache(size: number): Array<any> {
const hook = nextHook();
let memoCache: MemoCache;
if (
hook !== null &&
hook.updateQueue !== null &&
hook.updateQueue.memoCache != null
) {
memoCache = hook.updateQueue.memoCache;
} else {
memoCache = {
data: [],
index: 0,
};
}
let data = memoCache.data[memoCache.index];
if (data === undefined) {
const MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel');
data = new Array(size);
for (let i = 0; i < size; i++) {
data[i] = MEMO_CACHE_SENTINEL;
}
}
hookLog.push({
primitive: 'MemoCache',
stackError: new Error(),
value: data,
});
return data;
}
const Dispatcher: DispatcherType = {
use,
readContext,
useCacheRefresh,
useCallback,
@ -337,6 +391,7 @@ const Dispatcher: DispatcherType = {
useLayoutEffect,
useInsertionEffect,
useMemo,
useMemoCache,
useReducer,
useRef,
useState,
@ -513,10 +568,10 @@ function parseCustomHookName(functionName: void | string): string {
if (startIndex === -1) {
startIndex = 0;
}
if (functionName.substr(startIndex, 3) === 'use') {
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
startIndex += 3;
}
return functionName.substr(startIndex);
return functionName.slice(startIndex);
}
function buildTree(

View File

@ -14,6 +14,7 @@ let React;
let ReactTestRenderer;
let ReactDebugTools;
let act;
let useMemoCache;
describe('ReactHooksInspectionIntegration', () => {
beforeEach(() => {
@ -22,6 +23,7 @@ describe('ReactHooksInspectionIntegration', () => {
ReactTestRenderer = require('react-test-renderer');
act = require('internal-test-utils').act;
ReactDebugTools = require('react-debug-tools');
useMemoCache = React.unstable_useMemoCache;
});
it('should inspect the current state of useState hooks', async () => {
@ -633,6 +635,33 @@ describe('ReactHooksInspectionIntegration', () => {
});
});
// @gate enableUseMemoCacheHook
it('should support useMemoCache hook', () => {
function Foo() {
const $ = useMemoCache(1);
let t0;
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t0 = <div>{1}</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree.length).toEqual(1);
expect(tree[0].isStateEditable).toBe(false);
expect(tree[0].name).toBe('MemoCache');
expect(tree[0].value).toHaveLength(1);
expect(tree[0].value[0]).toEqual(<div>{1}</div>);
});
describe('useDebugValue', () => {
it('should support inspectable values for multiple custom hooks', () => {
function useLabeledValue(label) {
@ -869,7 +898,7 @@ describe('ReactHooksInspectionIntegration', () => {
const Suspense = React.Suspense;
function Foo(props) {
const [value] = React.useState(props.defaultValue.substr(0, 3));
const [value] = React.useState(props.defaultValue.slice(0, 3));
return <div>{value}</div>;
}
Foo.defaultProps = {

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools-core",
"version": "4.27.4",
"version": "4.27.8",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -167,7 +167,6 @@ function onDisconnected() {
disconnectedCallback();
}
// $FlowFixMe[missing-local-annot]
function onError({code, message}: $FlowFixMe) {
safeUnmount();

View File

@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "4.27.4",
"version_name": "4.27.4",
"version": "4.27.8",
"version_name": "4.27.8",
"minimum_chrome_version": "102",
"icons": {
"16": "icons/16-production.png",
@ -29,10 +29,7 @@
"resources": [
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/proxy.js",
"build/renderer.js",
"build/installHook.js"
"build/*.js"
],
"matches": [
"<all_urls>"

View File

@ -7,7 +7,7 @@ const {resolve} = require('path');
const {argv} = require('yargs');
const EXTENSION_PATH = resolve('./chrome/build/unpacked');
const START_URL = argv.url || 'https://reactjs.org/';
const START_URL = argv.url || 'https://react.dev/';
chromeLaunch(START_URL, {
args: [

View File

@ -27,7 +27,7 @@ const main = async buildId => {
const json = JSON.parse(file);
const alias = json.alias[0];
const commit = execSync('git rev-parse HEAD').toString().trim().substr(0, 7);
const commit = execSync('git rev-parse HEAD').toString().trim().slice(0, 7);
let date = new Date();
date = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;

View File

@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "4.27.4",
"version_name": "4.27.4",
"version": "4.27.8",
"version_name": "4.27.8",
"minimum_chrome_version": "102",
"icons": {
"16": "icons/16-production.png",
@ -29,10 +29,7 @@
"resources": [
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/proxy.js",
"build/renderer.js",
"build/installHook.js"
"build/*.js"
],
"matches": [
"<all_urls>"

View File

@ -9,7 +9,7 @@ const {resolve} = require('path');
const {argv} = require('yargs');
const EXTENSION_PATH = resolve('./edge/build/unpacked');
const START_URL = argv.url || 'https://reactjs.org/';
const START_URL = argv.url || 'https://react.dev/';
const extargs = `--load-extension=${EXTENSION_PATH}`;

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Firefox Developer Tools.",
"version": "4.27.4",
"version": "4.27.8",
"applications": {
"gecko": {
"id": "@react-devtools",
@ -30,10 +30,7 @@
"web_accessible_resources": [
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/proxy.js",
"build/renderer.js",
"build/installHook.js"
"build/*.js"
],
"background": {
"scripts": [

View File

@ -8,7 +8,7 @@ const {resolve} = require('path');
const {argv} = require('yargs');
const EXTENSION_PATH = resolve('./firefox/build/unpacked');
const START_URL = argv.url || 'https://reactjs.org/';
const START_URL = argv.url || 'https://react.dev/';
const firefoxVersion = process.env.WEB_EXT_FIREFOX;

View File

@ -11,7 +11,7 @@
/* eslint-disable no-unused-vars */
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
type JestMockFn<TArguments: $ReadOnlyArray<any>, TReturn> = {
(...args: TArguments): TReturn,
/**
* An object for introspecting mock calls
@ -626,7 +626,7 @@ interface JestExpectType {
* Use .toBeInstanceOf(Class) to check that an object is an instance of a
* class.
*/
toBeInstanceOf(cls: Class<*>): void;
toBeInstanceOf(cls: Class<any>): void;
/**
* .toBeNull() is the same as .toBe(null) but the error messages are a bit
* nicer.
@ -815,7 +815,7 @@ type JestObjectType = {
* Returns a new, unused mock function. Optionally takes a mock
* implementation.
*/
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
fn<TArguments: $ReadOnlyArray<any>, TReturn>(
implementation?: (...args: TArguments) => TReturn
): JestMockFn<TArguments, TReturn>,
/**

View File

@ -1,109 +1,34 @@
// Do not use imports or top-level requires here!
// Running module factories is intentionally delayed until we know the hook exists.
// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039
/**
* 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
*/
// @flow strict-local
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
'use strict';
import Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {initBackend} from 'react-devtools-shared/src/backend';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
let welcomeHasInitialized = false;
import {COMPACT_VERSION_NAME} from './utils';
// $FlowFixMe[missing-local-annot]
function welcome(event: $FlowFixMe) {
if (
event.source !== window ||
event.data.source !== 'react-devtools-content-script'
) {
return;
}
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
// In some circumstances, this method is called more than once for a single welcome message.
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
//
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
//
// The simplest solution is to ignore the duplicate events.
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
//
// See https://github.com/facebook/react/issues/24162
if (welcomeHasInitialized) {
console.warn(
'React DevTools detected duplicate welcome "message" events from the content script.',
);
return;
}
welcomeHasInitialized = true;
window.removeEventListener('message', welcome);
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
}
window.addEventListener('message', welcome);
function setup(hook: any) {
function setup(hook: ?DevToolsHook) {
if (hook == null) {
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return;
}
const Agent = require('react-devtools-shared/src/backend/agent').default;
const Bridge = require('react-devtools-shared/src/bridge').default;
const {initBackend} = require('react-devtools-shared/src/backend');
const setupNativeStyleEditor =
require('react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor').default;
const bridge = new Bridge<$FlowFixMe, $FlowFixMe>({
listen(fn) {
const listener = (event: $FlowFixMe) => {
if (
event.source !== window ||
!event.data ||
event.data.source !== 'react-devtools-content-script' ||
!event.data.payload
) {
return;
}
fn(event.data.payload);
};
window.addEventListener('message', listener);
return () => {
window.removeEventListener('message', listener);
};
},
send(event: string, payload: any, transferable?: Array<any>) {
window.postMessage(
{
source: 'react-devtools-bridge',
payload: {event, payload},
},
'*',
transferable,
);
},
hook.backends.set(COMPACT_VERSION_NAME, {
Agent,
Bridge,
initBackend,
setupNativeStyleEditor,
});
const agent = new Agent(bridge);
agent.addListener('shutdown', () => {
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
hook.emit('shutdown');
});
initBackend(hook, agent, window);
// Let the frontend know that the backend has attached listeners and is ready for messages.
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
bridge.send('extensionBackendInitialized');
// Setup React Native style editor if a renderer like react-native-web has injected it.
if (hook.resolveRNStyle) {
setupNativeStyleEditor(
bridge,
agent,
hook.resolveRNStyle,
hook.nativeStyleEditorValidAttributes,
);
}
hook.emit('devtools-backend-installed', COMPACT_VERSION_NAME);
}

View File

@ -0,0 +1,179 @@
/**
* 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 type {
DevToolsHook,
ReactRenderer,
} from 'react-devtools-shared/src/backend/types';
import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
import {COMPACT_VERSION_NAME} from './utils';
let welcomeHasInitialized = false;
function welcome(event: $FlowFixMe) {
if (
event.source !== window ||
event.data.source !== 'react-devtools-content-script'
) {
return;
}
// In some circumstances, this method is called more than once for a single welcome message.
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
//
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
//
// The simplest solution is to ignore the duplicate events.
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
//
// See https://github.com/facebook/react/issues/24162
if (welcomeHasInitialized) {
console.warn(
'React DevTools detected duplicate welcome "message" events from the content script.',
);
return;
}
welcomeHasInitialized = true;
window.removeEventListener('message', welcome);
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
}
window.addEventListener('message', welcome);
function setup(hook: ?DevToolsHook) {
// this should not happen, but Chrome can be weird sometimes
if (hook == null) {
return;
}
// register renderers that have already injected themselves.
hook.renderers.forEach(renderer => {
registerRenderer(renderer, hook);
});
// Activate and remove from required all present backends, registered within the hook
hook.backends.forEach((_, backendVersion) => {
requiredBackends.delete(backendVersion);
activateBackend(backendVersion, hook);
});
updateRequiredBackends();
// register renderers that inject themselves later.
hook.sub('renderer', ({renderer}) => {
registerRenderer(renderer, hook);
updateRequiredBackends();
});
// listen for backend installations.
hook.sub('devtools-backend-installed', version => {
activateBackend(version, hook);
updateRequiredBackends();
});
}
const requiredBackends = new Set<string>();
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
let version = renderer.reconcilerVersion || renderer.version;
if (!hasAssignedBackend(version)) {
version = COMPACT_VERSION_NAME;
}
// Check if required backend is already activated, no need to require again
if (!hook.backends.has(version)) {
requiredBackends.add(version);
}
}
function activateBackend(version: string, hook: DevToolsHook) {
const backend = hook.backends.get(version);
if (!backend) {
throw new Error(`Could not find backend for version "${version}"`);
}
const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
const bridge = new Bridge({
listen(fn) {
const listener = (event: $FlowFixMe) => {
if (
event.source !== window ||
!event.data ||
event.data.source !== 'react-devtools-content-script' ||
!event.data.payload
) {
return;
}
fn(event.data.payload);
};
window.addEventListener('message', listener);
return () => {
window.removeEventListener('message', listener);
};
},
send(event: string, payload: any, transferable?: Array<any>) {
window.postMessage(
{
source: 'react-devtools-bridge',
payload: {event, payload},
},
'*',
transferable,
);
},
});
const agent = new Agent(bridge);
agent.addListener('shutdown', () => {
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
hook.emit('shutdown');
});
initBackend(hook, agent, window);
// Setup React Native style editor if a renderer like react-native-web has injected it.
if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) {
setupNativeStyleEditor(
bridge,
agent,
hook.resolveRNStyle,
hook.nativeStyleEditorValidAttributes,
);
}
// Let the frontend know that the backend has attached listeners and is ready for messages.
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
bridge.send('extensionBackendInitialized');
// this backend is activated
requiredBackends.delete(version);
}
// tell the service worker which versions of backends are needed for the current page
function updateRequiredBackends() {
if (requiredBackends.size === 0) {
return;
}
window.postMessage(
{
source: 'react-devtools-backend-manager',
payload: {
type: 'react-devtools-required-backends',
versions: Array.from(requiredBackends),
},
},
'*',
);
}

View File

@ -2,42 +2,52 @@
'use strict';
import {IS_FIREFOX} from './utils';
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from './utils';
const ports = {};
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
// Manifest V3 method of injecting content scripts (not yet supported in Firefox)
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
// It's critical since it allows us to directly run scripts on the "main" world on the page
// "document_start" allows it to run before the page's scripts
// so the hook can be detected by react reconciler
chrome.scripting.registerContentScripts(
[
{
id: 'hook',
matches: ['<all_urls>'],
js: ['build/installHook.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
{
id: 'renderer',
matches: ['<all_urls>'],
js: ['build/renderer.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
],
function () {
// When the content scripts are already registered, an error will be thrown.
// It happens when the service worker process is incorrectly duplicated.
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
async function dynamicallyInjectContentScripts() {
const contentScriptsToInject = [
{
id: 'hook',
matches: ['<all_urls>'],
js: ['build/installHook.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
);
{
id: 'renderer',
matches: ['<all_urls>'],
js: ['build/renderer.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
];
try {
// For some reason dynamically injected scripts might be already registered
// Registering them again will fail, which will result into
// __REACT_DEVTOOLS_GLOBAL_HOOK__ hook not being injected
// Not specifying ids, because Chrome throws an error
// if id of non-injected script is provided
await chrome.scripting.unregisterContentScripts();
// equivalent logic for Firefox is in prepareInjection.js
// Manifest V3 method of injecting content script
// TODO(hoxyq): migrate Firefox to V3 manifests
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
// It's critical since it allows us to directly run scripts on the "main" world on the page
// "document_start" allows it to run before the page's scripts
// so the hook can be detected by react reconciler
await chrome.scripting.registerContentScripts(contentScriptsToInject);
} catch (error) {
console.error(error);
}
}
if (!IS_FIREFOX) {
dynamicallyInjectContentScripts();
}
chrome.runtime.onConnect.addListener(function (port) {
@ -172,6 +182,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
chrome.runtime.onMessage.addListener((request, sender) => {
const tab = sender.tab;
// sender.tab.id from content script points to the tab that injected the content script
if (tab) {
const id = tab.id;
// This is sent from the hook content script.
@ -179,26 +190,53 @@ chrome.runtime.onMessage.addListener((request, sender) => {
if (request.hasDetectedReact) {
setIconAndPopup(request.reactBuildType, id);
} else {
const devtools = ports[id]?.devtools;
switch (request.payload?.type) {
case 'fetch-file-with-cache-complete':
case 'fetch-file-with-cache-error':
// Forward the result of fetch-in-page requests back to the extension.
const devtools = ports[id]?.devtools;
if (devtools) {
devtools.postMessage(request);
}
devtools?.postMessage(request);
break;
// This is sent from the backend manager running on a page
case 'react-devtools-required-backends':
const backendsToDownload = [];
request.payload.versions.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
chrome.scripting.executeScript({
target: {tabId: id},
files: [`/build/react_devtools_backend_${version}.js`],
world: chrome.scripting.ExecutionWorld.MAIN,
});
}
} else {
backendsToDownload.push(version);
}
});
// Request the necessary backends in the extension DevTools UI
// TODO: handle this message in main.js to build the UI
devtools?.postMessage({
payload: {
type: 'react-devtools-additional-backends',
versions: backendsToDownload,
},
});
break;
}
}
} else if (request.payload?.tabId) {
}
// sender.tab.id from devtools page may not exist, or point to the undocked devtools window
// so we use the payload to get the tab id
if (request.payload?.tabId) {
const tabId = request.payload?.tabId;
// This is sent from the devtools page when it is ready for injecting the backend
if (request.payload.type === 'react-devtools-inject-backend') {
if (request.payload.type === 'react-devtools-inject-backend-manager') {
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
chrome.scripting.executeScript({
target: {tabId},
files: ['/build/react_devtools_backend.js'],
files: ['/build/backendManager.js'],
world: chrome.scripting.ExecutionWorld.MAIN,
});
}

View File

@ -3,7 +3,7 @@
import nullthrows from 'nullthrows';
import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
import {IS_FIREFOX} from '../utils';
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';
// We run scripts on the page via the service worker (backgroud.js) for
// Manifest V3 extensions (Chrome & Edge).
@ -90,11 +90,22 @@ window.addEventListener('message', function onMessage({data, source}) {
);
}
break;
case 'react-devtools-inject-backend':
case 'react-devtools-inject-backend-manager':
if (IS_FIREFOX) {
injectScriptSync(
chrome.runtime.getURL('build/react_devtools_backend.js'),
);
injectScriptSync(chrome.runtime.getURL('build/backendManager.js'));
}
break;
case 'react-devtools-backend-manager':
if (IS_FIREFOX) {
data.payload?.versions?.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
injectScriptSync(
chrome.runtime.getURL(
`/build/react_devtools_backend_${version}.js`,
),
);
}
});
}
break;
}

View File

@ -5,7 +5,7 @@
let backendDisconnected: boolean = false;
let backendInitialized: boolean = false;
function sayHelloToBackend() {
function sayHelloToBackendManager() {
window.postMessage(
{
source: 'react-devtools-content-script',
@ -26,14 +26,19 @@ function handleMessageFromDevtools(message) {
}
function handleMessageFromPage(event) {
if (
event.source === window &&
event.data &&
event.data.source === 'react-devtools-bridge'
) {
backendInitialized = true;
if (event.source === window && event.data) {
// This is a message from a bridge (initialized by a devtools backend)
if (event.data.source === 'react-devtools-bridge') {
backendInitialized = true;
port.postMessage(event.data.payload);
port.postMessage(event.data.payload);
}
// This is a message from the backend manager
if (event.data.source === 'react-devtools-backend-manager') {
chrome.runtime.sendMessage({
payload: event.data.payload,
});
}
}
}
@ -63,17 +68,17 @@ port.onDisconnect.addListener(handleDisconnect);
window.addEventListener('message', handleMessageFromPage);
sayHelloToBackend();
sayHelloToBackendManager();
// The backend waits to install the global hook until notified by the content script.
// In the event of a page reload, the content script might be loaded before the backend is injected.
// Because of this we need to poll the backend until it has been initialized.
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
// Because of this we need to poll the backend manager until it has been initialized.
if (!backendInitialized) {
const intervalID = setInterval(() => {
if (backendInitialized || backendDisconnected) {
clearInterval(intervalID);
} else {
sayHelloToBackend();
sayHelloToBackendManager();
}
}, 500);
}

View File

@ -191,7 +191,7 @@ function createPanelIfReactLoaded() {
chrome.runtime.sendMessage({
source: 'react-devtools-main',
payload: {
type: 'react-devtools-inject-backend',
type: 'react-devtools-inject-backend-manager',
tabId,
},
});
@ -199,7 +199,7 @@ function createPanelIfReactLoaded() {
// Firefox does not support executing script in ExecutionWorld.MAIN from content script.
// see prepareInjection.js
chrome.devtools.inspectedWindow.eval(
`window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
`window.postMessage({ source: 'react-devtools-inject-backend-manager' }, '*');`,
function (response, evalError) {
if (evalError) {
console.error(evalError);

View File

@ -41,3 +41,6 @@ export function getBrowserTheme(): BrowserTheme {
}
}
}
export const COMPACT_VERSION_NAME = 'compact';
export const EXTENSION_CONTAINED_VERSIONS = [COMPACT_VERSION_NAME];

View File

@ -42,7 +42,7 @@ module.exports = {
},
output: {
path: __dirname + '/build',
filename: 'react_devtools_backend.js',
filename: 'react_devtools_backend_compact.js',
},
node: {
// Don't define a polyfill on window.setImmediate

View File

@ -51,6 +51,7 @@ module.exports = {
devtool: __DEV__ ? 'cheap-module-source-map' : false,
entry: {
background: './src/background.js',
backendManager: './src/backendManager.js',
main: './src/main.js',
panel: './src/panel.js',
proxy: './src/contentScripts/proxy.js',

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools-inline",
"version": "4.27.4",
"version": "4.27.8",
"description": "Embed react-devtools within a website",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -1243,10 +1243,9 @@ describe('Timeline profiler', () => {
function Example() {
const setHigh = React.useState(0)[1];
const setLow = React.useState(0)[1];
const startTransition = React.useTransition()[1];
updaterFn = () => {
startTransition(() => {
React.startTransition(() => {
setLow(prevLow => prevLow + 1);
});
setHigh(prevHigh => prevHigh + 1);
@ -1265,24 +1264,6 @@ describe('Timeline profiler', () => {
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000001000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000010000000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "

View File

@ -13,7 +13,7 @@ function serializeHook(hook) {
// Remove user-specific portions of this file path.
let fileName = hook.hookSource.fileName;
const index = fileName.lastIndexOf('/react-devtools-shared/');
fileName = fileName.substring(index + 1);
fileName = fileName.slice(index + 1);
let subHooks = hook.subHooks;
if (subHooks) {

View File

@ -938,8 +938,10 @@ describe('ProfilingCache', () => {
}
});
// @reactVersion >= 18.0
// @reactVersion = 17.0
it('should handle unexpectedly shallow suspense trees', () => {
// This test only runs in v17 because it's a regression test for legacy
// Suspense behavior, and the implementation details changed in v18.
const container = document.createElement('div');
utils.act(() => store.profilerStore.startProfiling());
@ -965,15 +967,7 @@ describe('ProfilingCache', () => {
"passiveEffectDuration": null,
"priorityLevel": "Normal",
"timestamp": 0,
"updaters": [
{
"displayName": "render()",
"hocDisplayNames": null,
"id": 1,
"key": null,
"type": 11,
},
],
"updaters": null,
},
]
`);

View File

@ -16,6 +16,7 @@ import type {RendererID} from '../types';
import type {StyleAndLayout} from './types';
export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
export type SetupNativeStyleEditor = typeof setupNativeStyleEditor;
export default function setupNativeStyleEditor(
bridge: BackendBridge,

View File

@ -11,9 +11,17 @@ import Agent from './agent';
import {attach} from './renderer';
import {attach as attachLegacy} from './legacy/renderer';
import {hasAssignedBackend} from './utils';
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
// this is the backend that is compatible with all older React versions
function isMatchingRender(version: string): boolean {
return !hasAssignedBackend(version);
}
export type InitBackend = typeof initBackend;
export function initBackend(
hook: DevToolsHook,
agent: Agent,
@ -23,6 +31,7 @@ export function initBackend(
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return () => {};
}
const subs = [
hook.sub(
'renderer-attached',
@ -56,6 +65,10 @@ export function initBackend(
];
const attachRenderer = (id: number, renderer: ReactRenderer) => {
// only attach if the renderer is compatible with the current version of the backend
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
return;
}
let rendererInterface = hook.rendererInterfaces.get(id);
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)

View File

@ -244,7 +244,6 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -281,7 +280,6 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -318,7 +316,6 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -350,7 +347,6 @@ export function attach(
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {

View File

@ -54,7 +54,10 @@ let supportsUserTiming =
let supportsUserTimingV3 = false;
if (supportsUserTiming) {
const CHECK_V3_MARK = '__v3';
const markOptions = ({}: {startTime?: number});
const markOptions: {
detail?: mixed,
startTime?: number,
} = {};
Object.defineProperty(markOptions, 'startTime', {
get: function () {
supportsUserTimingV3 = true;
@ -64,7 +67,6 @@ if (supportsUserTiming) {
});
try {
// $FlowFixMe[extra-arg]: Flow expects the User Timing level 2 API.
performance.mark(CHECK_V3_MARK, markOptions);
} catch (error) {
// Ignore

View File

@ -92,10 +92,7 @@ import {
SERVER_CONTEXT_SYMBOL_STRING,
} from './ReactSymbols';
import {format} from './utils';
import {
enableProfilerChangedHookIndices,
enableStyleXFeatures,
} from 'react-devtools-feature-flags';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';
import {getStyleXData} from './StyleX/utils';
@ -1265,19 +1262,12 @@ export function attach(
};
// Only traverse the hooks list once, depending on what info we're returning.
if (enableProfilerChangedHookIndices) {
const indices = getChangedHooksIndices(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
data.hooks = indices;
data.didHooksChange = indices !== null && indices.length > 0;
} else {
data.didHooksChange = didHooksChange(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
}
const indices = getChangedHooksIndices(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
data.hooks = indices;
data.didHooksChange = indices !== null && indices.length > 0;
return data;
}
@ -1458,12 +1448,13 @@ export function attach(
return false;
}
function didHooksChange(prev: any, next: any): boolean {
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
if (prev == null || next == null) {
return false;
return null;
}
// We can't report anything meaningful for hooks changes.
const indices = [];
let index = 0;
if (
next.hasOwnProperty('baseState') &&
next.hasOwnProperty('memoizedState') &&
@ -1472,45 +1463,15 @@ export function attach(
) {
while (next !== null) {
if (didStatefulHookChange(prev, next)) {
return true;
} else {
next = next.next;
prev = prev.next;
indices.push(index);
}
next = next.next;
prev = prev.next;
index++;
}
}
return false;
}
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
if (enableProfilerChangedHookIndices) {
if (prev == null || next == null) {
return null;
}
const indices = [];
let index = 0;
if (
next.hasOwnProperty('baseState') &&
next.hasOwnProperty('memoizedState') &&
next.hasOwnProperty('next') &&
next.hasOwnProperty('queue')
) {
while (next !== null) {
if (didStatefulHookChange(prev, next)) {
indices.push(index);
}
next = next.next;
prev = prev.next;
index++;
}
}
return indices;
}
return null;
return indices;
}
function getChangedKeys(prev: any, next: any): null | Array<string> {
@ -4354,7 +4315,7 @@ export function attach(
if (pseudoKey === undefined) {
throw new Error('Expected root pseudo key to be known.');
}
const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':'));
const name = pseudoKey.slice(0, pseudoKey.lastIndexOf(':'));
const counter = rootDisplayNameCounter.get(name);
if (counter === undefined) {
throw new Error('Expected counter to be known.');

View File

@ -22,9 +22,15 @@ import type {
ElementType,
Plugins,
} from 'react-devtools-shared/src/types';
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import type {
ResolveNativeStyle,
SetupNativeStyleEditor,
} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import type {InitBackend} from 'react-devtools-shared/src/backend';
import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
import type {BrowserTheme} from 'react-devtools-shared/src/types';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type Agent from './agent';
type BundleType =
| 0 // PROD
@ -464,10 +470,18 @@ export type DevToolsProfilingHooks = {
markComponentPassiveEffectUnmountStopped: () => void,
};
export type DevToolsBackend = {
Agent: Class<Agent>,
Bridge: Class<BackendBridge>,
initBackend: InitBackend,
setupNativeStyleEditor?: SetupNativeStyleEditor,
};
export type DevToolsHook = {
listeners: {[key: string]: Array<Handler>, ...},
rendererInterfaces: Map<RendererID, RendererInterface>,
renderers: Map<RendererID, ReactRenderer>,
backends: Map<string, DevToolsBackend>,
emit: (event: string, data: any) => void,
getFiberRoots: (rendererID: RendererID) => Set<Object>,

View File

@ -14,6 +14,15 @@ import isArray from 'shared/isArray';
import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
// TODO: update this to the first React version that has a corresponding DevTools backend
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
export function hasAssignedBackend(version?: string): boolean {
if (version == null || version === '') {
return false;
}
return gte(version, FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER);
}
export function cleanForBridge(
data: Object | null,
isPathAllowed: (path: Array<string | number>) => boolean,

View File

@ -15,11 +15,8 @@
export const consoleManagedByDevToolsDuringStrictMode = false;
export const enableLogger = true;
export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = true;
export const isInternalFacebookBuild = true;
export const enableProfilerComponentTree = true;
/************************************************************************
* Do not edit the code below.

View File

@ -15,11 +15,8 @@
export const consoleManagedByDevToolsDuringStrictMode = false;
export const enableLogger = false;
export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = false;
export const isInternalFacebookBuild = false;
export const enableProfilerComponentTree = true;
/************************************************************************
* Do not edit the code below.

View File

@ -15,8 +15,5 @@
export const consoleManagedByDevToolsDuringStrictMode = true;
export const enableLogger = false;
export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = false;
export const isInternalFacebookBuild = false;
export const enableProfilerComponentTree = true;

View File

@ -15,11 +15,8 @@
export const consoleManagedByDevToolsDuringStrictMode = true;
export const enableLogger = true;
export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = true;
export const isInternalFacebookBuild = true;
export const enableProfilerComponentTree = true;
/************************************************************************
* Do not edit the code below.

View File

@ -15,11 +15,8 @@
export const consoleManagedByDevToolsDuringStrictMode = true;
export const enableLogger = false;
export const enableNamedHooksFeature = true;
export const enableProfilerChangedHookIndices = true;
export const enableStyleXFeatures = false;
export const isInternalFacebookBuild = false;
export const enableProfilerComponentTree = true;
/************************************************************************
* Do not edit the code below.

View File

@ -169,7 +169,7 @@ export default class Store extends EventEmitter<{
// Renderer ID is needed to support inspection fiber props, state, and hooks.
_rootIDToRendererID: Map<number, number> = new Map();
// These options may be initially set by a confiugraiton option when constructing the Store.
// These options may be initially set by a configuration option when constructing the Store.
_supportsNativeInspection: boolean = true;
_supportsProfiling: boolean = false;
_supportsReloadAndProfile: boolean = false;
@ -486,7 +486,7 @@ export default class Store extends EventEmitter<{
}
containsElement(id: number): boolean {
return this._idToElement.get(id) != null;
return this._idToElement.has(id);
}
getElementAtIndex(index: number): Element | null {
@ -539,13 +539,13 @@ export default class Store extends EventEmitter<{
}
getElementIDAtIndex(index: number): number | null {
const element: Element | null = this.getElementAtIndex(index);
const element = this.getElementAtIndex(index);
return element === null ? null : element.id;
}
getElementByID(id: number): Element | null {
const element = this._idToElement.get(id);
if (element == null) {
if (element === undefined) {
console.warn(`No element found with id "${id}"`);
return null;
}
@ -607,7 +607,10 @@ export default class Store extends EventEmitter<{
let currentID = element.parentID;
let index = 0;
while (true) {
const current = ((this._idToElement.get(currentID): any): Element);
const current = this._idToElement.get(currentID);
if (current === undefined) {
return null;
}
const {children} = current;
for (let i = 0; i < children.length; i++) {
@ -615,7 +618,12 @@ export default class Store extends EventEmitter<{
if (childID === previousID) {
break;
}
const child = ((this._idToElement.get(childID): any): Element);
const child = this._idToElement.get(childID);
if (child === undefined) {
return null;
}
index += child.isCollapsed ? 1 : child.weight;
}
@ -637,7 +645,12 @@ export default class Store extends EventEmitter<{
if (rootID === currentID) {
break;
}
const root = ((this._idToElement.get(rootID): any): Element);
const root = this._idToElement.get(rootID);
if (root === undefined) {
return null;
}
index += root.weight;
}
@ -647,7 +660,7 @@ export default class Store extends EventEmitter<{
getOwnersListForElement(ownerID: number): Array<Element> {
const list: Array<Element> = [];
const element = this._idToElement.get(ownerID);
if (element != null) {
if (element !== undefined) {
list.push({
...element,
depth: 0,
@ -665,8 +678,8 @@ export default class Store extends EventEmitter<{
// Seems better to defer the cost, since the set of ids is probably pretty small.
const sortedIDs = Array.from(unsortedIDs).sort(
(idA, idB) =>
((this.getIndexOfElementID(idA): any): number) -
((this.getIndexOfElementID(idB): any): number),
(this.getIndexOfElementID(idA) || 0) -
(this.getIndexOfElementID(idB) || 0),
);
// Next we need to determine the appropriate depth for each element in the list.
@ -677,7 +690,7 @@ export default class Store extends EventEmitter<{
// at which point, our depth is just the depth of that node plus one.
sortedIDs.forEach(id => {
const innerElement = this._idToElement.get(id);
if (innerElement != null) {
if (innerElement !== undefined) {
let parentID = innerElement.parentID;
let depth = 0;
@ -689,7 +702,7 @@ export default class Store extends EventEmitter<{
break;
}
const parent = this._idToElement.get(parentID);
if (parent == null) {
if (parent === undefined) {
break;
}
parentID = parent.parentID;
@ -710,7 +723,7 @@ export default class Store extends EventEmitter<{
getRendererIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current != null) {
while (current !== undefined) {
if (current.parentID === 0) {
const rendererID = this._rootIDToRendererID.get(current.id);
return rendererID == null ? null : rendererID;
@ -723,7 +736,7 @@ export default class Store extends EventEmitter<{
getRootIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current != null) {
while (current !== undefined) {
if (current.parentID === 0) {
return current.id;
} else {
@ -765,10 +778,8 @@ export default class Store extends EventEmitter<{
const weightDelta = 1 - element.weight;
let parentElement: void | Element = ((this._idToElement.get(
element.parentID,
): any): Element);
while (parentElement != null) {
let parentElement = this._idToElement.get(element.parentID);
while (parentElement !== undefined) {
// We don't need to break on a collapsed parent in the same way as the expand case below.
// That's because collapsing a node doesn't "bubble" and affect its parents.
parentElement.weight += weightDelta;
@ -776,7 +787,7 @@ export default class Store extends EventEmitter<{
}
}
} else {
let currentElement = element;
let currentElement: ?Element = element;
while (currentElement != null) {
const oldWeight = currentElement.isCollapsed
? 1
@ -791,10 +802,8 @@ export default class Store extends EventEmitter<{
: currentElement.weight;
const weightDelta = newWeight - oldWeight;
let parentElement: void | Element = ((this._idToElement.get(
currentElement.parentID,
): any): Element);
while (parentElement != null) {
let parentElement = this._idToElement.get(currentElement.parentID);
while (parentElement !== undefined) {
parentElement.weight += weightDelta;
if (parentElement.isCollapsed) {
// It's important to break on a collapsed parent when expanding nodes.
@ -808,10 +817,8 @@ export default class Store extends EventEmitter<{
currentElement =
currentElement.parentID !== 0
? // $FlowFixMe[incompatible-type] found when upgrading Flow
this.getElementByID(currentElement.parentID)
: // $FlowFixMe[incompatible-type] found when upgrading Flow
null;
? this.getElementByID(currentElement.parentID)
: null;
}
}
@ -833,7 +840,7 @@ export default class Store extends EventEmitter<{
}
_adjustParentTreeWeight: (
parentElement: Element | null,
parentElement: ?Element,
weightDelta: number,
) => void = (parentElement, weightDelta) => {
let isInsideCollapsedSubTree = false;
@ -848,9 +855,7 @@ export default class Store extends EventEmitter<{
break;
}
parentElement = ((this._idToElement.get(
parentElement.parentID,
): any): Element);
parentElement = this._idToElement.get(parentElement.parentID);
}
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
@ -906,13 +911,16 @@ export default class Store extends EventEmitter<{
const stringTable: Array<string | null> = [
null, // ID = 0 corresponds to the null string.
];
const stringTableSize = operations[i++];
const stringTableSize = operations[i];
i++;
const stringTableEnd = i + stringTableSize;
while (i < stringTableEnd) {
const nextLength = operations[i++];
const nextString = utfDecodeString(
(operations.slice(i, i + nextLength): any),
);
const nextLength = operations[i];
i++;
const nextString = utfDecodeString(operations.slice(i, i + nextLength));
stringTable.push(nextString);
i += nextLength;
}
@ -921,7 +929,7 @@ export default class Store extends EventEmitter<{
const operation = operations[i];
switch (operation) {
case TREE_OPERATION_ADD: {
const id = ((operations[i + 1]: any): number);
const id = operations[i + 1];
const type = ((operations[i + 2]: any): ElementType);
i += 3;
@ -934,8 +942,6 @@ export default class Store extends EventEmitter<{
);
}
let ownerID: number = 0;
let parentID: number = ((null: any): number);
if (type === ElementTypeRoot) {
if (__DEBUG__) {
debug('Add', `new root node ${id}`);
@ -997,10 +1003,10 @@ export default class Store extends EventEmitter<{
haveRootsChanged = true;
} else {
parentID = ((operations[i]: any): number);
const parentID = operations[i];
i++;
ownerID = ((operations[i]: any): number);
const ownerID = operations[i];
i++;
const displayNameStringID = operations[i];
@ -1018,17 +1024,17 @@ export default class Store extends EventEmitter<{
);
}
if (!this._idToElement.has(parentID)) {
const parentElement = this._idToElement.get(parentID);
if (parentElement === undefined) {
this._throwAndEmitError(
Error(
`Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
),
);
continue;
}
const parentElement = ((this._idToElement.get(
parentID,
): any): Element);
parentElement.children.push(id);
const [displayNameWithoutHOCs, hocDisplayNames] =
@ -1065,23 +1071,25 @@ export default class Store extends EventEmitter<{
break;
}
case TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
const removeLength = operations[i + 1];
i += 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = ((operations[i]: any): number);
const id = operations[i];
const element = this._idToElement.get(id);
if (!this._idToElement.has(id)) {
if (element === undefined) {
this._throwAndEmitError(
Error(
`Cannot remove node "${id}" because no matching node was found in the Store.`,
),
);
continue;
}
i += 1;
const element = ((this._idToElement.get(id): any): Element);
const {children, ownerID, parentID, weight} = element;
if (children.length > 0) {
this._throwAndEmitError(
@ -1091,7 +1099,7 @@ export default class Store extends EventEmitter<{
this._idToElement.delete(id);
let parentElement = null;
let parentElement: ?Element = null;
if (parentID === 0) {
if (__DEBUG__) {
debug('Remove', `node ${id} root`);
@ -1106,14 +1114,18 @@ export default class Store extends EventEmitter<{
if (__DEBUG__) {
debug('Remove', `node ${id} from parent ${parentID}`);
}
parentElement = ((this._idToElement.get(parentID): any): Element);
parentElement = this._idToElement.get(parentID);
if (parentElement === undefined) {
this._throwAndEmitError(
Error(
`Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
),
);
continue;
}
const index = parentElement.children.indexOf(id);
parentElement.children.splice(index, 1);
}
@ -1167,19 +1179,21 @@ export default class Store extends EventEmitter<{
break;
}
case TREE_OPERATION_REORDER_CHILDREN: {
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
const id = operations[i + 1];
const numChildren = operations[i + 2];
i += 3;
if (!this._idToElement.has(id)) {
const element = this._idToElement.get(id);
if (element === undefined) {
this._throwAndEmitError(
Error(
`Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
),
);
continue;
}
const element = ((this._idToElement.get(id): any): Element);
const children = element.children;
if (children.length !== numChildren) {
this._throwAndEmitError(

View File

@ -155,7 +155,7 @@ export function sanitizeForParse(value: any): any | string {
value.charAt(0) === "'" &&
value.charAt(value.length - 1) === "'"
) {
return '"' + value.substr(1, value.length - 2) + '"';
return '"' + value.slice(1, value.length - 1) + '"';
}
}
return value;

View File

@ -237,7 +237,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
strictModeBadge = (
<a
className={styles.StrictModeNonCompliant}
href="https://fb.me/devtools-strict-mode"
href="https://react.dev/reference/react/StrictMode"
rel="noopener noreferrer"
target="_blank"
title="This component is not running in StrictMode. Click to learn more.">

View File

@ -36,7 +36,6 @@ import {loadModule} from 'react-devtools-shared/src/dynamicImportCache';
import FetchFileWithCachingContext from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import {SettingsContext} from '../Settings/SettingsContext';
import {enableNamedHooksFeature} from 'react-devtools-feature-flags';
import type {HookNames} from 'react-devtools-shared/src/types';
import type {ReactNodeList} from 'shared/ReactTypes';
@ -128,28 +127,26 @@ export function InspectedElementContextController({
if (!elementHasChanged && element !== null) {
inspectedElement = inspectElement(element, state.path, store, bridge);
if (enableNamedHooksFeature) {
if (typeof hookNamesModuleLoader === 'function') {
if (parseHookNames || alreadyLoadedHookNames) {
const hookNamesModule = loadModule(hookNamesModuleLoader);
if (hookNamesModule !== null) {
const {parseHookNames: loadHookNamesFunction, purgeCachedMetadata} =
hookNamesModule;
if (typeof hookNamesModuleLoader === 'function') {
if (parseHookNames || alreadyLoadedHookNames) {
const hookNamesModule = loadModule(hookNamesModuleLoader);
if (hookNamesModule !== null) {
const {parseHookNames: loadHookNamesFunction, purgeCachedMetadata} =
hookNamesModule;
purgeCachedMetadataRef.current = purgeCachedMetadata;
purgeCachedMetadataRef.current = purgeCachedMetadata;
if (
inspectedElement !== null &&
inspectedElement.hooks !== null &&
loadHookNamesFunction !== null
) {
hookNames = loadHookNames(
element,
inspectedElement.hooks,
loadHookNamesFunction,
fetchFileWithCaching,
);
}
if (
inspectedElement !== null &&
inspectedElement.hooks !== null &&
loadHookNamesFunction !== null
) {
hookNames = loadHookNames(
element,
inspectedElement.hooks,
loadHookNamesFunction,
fetchFileWithCaching,
);
}
}
}

View File

@ -22,10 +22,6 @@ import styles from './InspectedElementHooksTree.css';
import useContextMenu from '../../ContextMenu/useContextMenu';
import {meta} from '../../../hydration';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
import {
enableNamedHooksFeature,
enableProfilerChangedHookIndices,
} from 'react-devtools-feature-flags';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import isArray from 'react-devtools-shared/src/isArray';
@ -90,8 +86,7 @@ export function InspectedElementHooksTree({
data-testname="InspectedElementHooksTree">
<div className={styles.HeaderRow}>
<div className={styles.Header}>hooks</div>
{enableNamedHooksFeature &&
typeof hookNamesModuleLoader === 'function' &&
{typeof hookNamesModuleLoader === 'function' &&
(!parseHookNames || hookParsingFailed) && (
<Toggle
className={hookParsingFailed ? styles.ToggleError : null}
@ -225,15 +220,13 @@ function HookView({
const isCustomHook = subHooks.length > 0;
let name = hook.name;
if (enableProfilerChangedHookIndices) {
if (hookID !== null) {
name = (
<>
<span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
{name}
</>
);
}
if (hookID !== null) {
name = (
<>
<span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
{name}
</>
);
}
const type = typeof value;

View File

@ -45,7 +45,6 @@ const resource: Resource<
(element: Element) => {
const request = inProgressRequests.get(element);
if (request != null) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
return request.promise;
}

View File

@ -34,7 +34,6 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools
import portaledContent from '../portaledContent';
import {StoreContext} from '../context';
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
import {enableProfilerComponentTree} from 'react-devtools-feature-flags';
import styles from './Profiler.css';
@ -56,8 +55,6 @@ function Profiler(_: {}) {
const {supportsTimeline} = useContext(StoreContext);
const isLegacyProfilerSelected = selectedTabID !== 'timeline';
const isRightColumnVisible =
isLegacyProfilerSelected || enableProfilerComponentTree;
let view = null;
if (didRecordCommits || selectedTabID === 'timeline') {
@ -151,9 +148,7 @@ function Profiler(_: {}) {
<ModalDialog />
</div>
</div>
{isRightColumnVisible && (
<div className={styles.RightColumn}>{sidebar}</div>
)}
<div className={styles.RightColumn}>{sidebar}</div>
<SettingsModal />
</div>
</SettingsModalContextController>

View File

@ -9,7 +9,6 @@
import * as React from 'react';
import {useContext} from 'react';
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
import {ProfilerContext} from '../Profiler/ProfilerContext';
import {StoreContext} from '../context';
@ -103,7 +102,7 @@ export default function WhatChanged({fiberID}: Props): React.Node {
}
if (didHooksChange) {
if (enableProfilerChangedHookIndices && Array.isArray(hooks)) {
if (Array.isArray(hooks)) {
changes.push(
<div key="hooks" className={styles.Item}>
{hookIndicesToString(hooks)}

View File

@ -36,10 +36,10 @@ export function createRegExp(string: string): RegExp {
// Allow /regex/ syntax with optional last /
if (string[0] === '/') {
// Cut off first slash
string = string.substring(1);
string = string.slice(1);
// Cut off last slash, but only if it's there
if (string[string.length - 1] === '/') {
string = string.substring(0, string.length - 1);
string = string.slice(0, string.length - 1);
}
try {
return new RegExp(string, 'i');
@ -186,9 +186,9 @@ export function truncateText(text: string, maxLength: number): string {
const {length} = text;
if (length > maxLength) {
return (
text.substr(0, Math.floor(maxLength / 2)) +
text.slice(0, Math.floor(maxLength / 2)) +
'…' +
text.substr(length - Math.ceil(maxLength / 2) - 1)
text.slice(length - Math.ceil(maxLength / 2) - 1)
);
} else {
return text;

View File

@ -15,6 +15,7 @@ import type {
ReactRenderer,
RendererID,
RendererInterface,
DevToolsBackend,
} from './backend/types';
declare var window: any;
@ -506,11 +507,14 @@ export function installHook(target: any): DevToolsHook | null {
const rendererInterfaces = new Map<RendererID, RendererInterface>();
const listeners: {[string]: Array<Handler>} = {};
const renderers = new Map<RendererID, ReactRenderer>();
const backends = new Map<string, DevToolsBackend>();
const hook: DevToolsHook = {
rendererInterfaces,
listeners,
backends,
// Fast Refresh for web relies on this.
renderers,

View File

@ -693,7 +693,7 @@ function truncateForDisplay(
length: number = MAX_PREVIEW_STRING_LENGTH,
) {
if (string.length > length) {
return string.substr(0, length) + '…';
return string.slice(0, length) + '…';
} else {
return string;
}

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "react-devtools-timeline",
"version": "4.27.4",
"version": "4.27.8",
"license": "MIT",
"dependencies": {
"@elg/speedscope": "1.9.0-a6f84db",

View File

@ -249,7 +249,7 @@ const TooltipNetworkMeasure = ({
let urlToDisplay = url;
if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half);
urlToDisplay = url.slice(0, half) + '…' + url.slice(url.length - half);
}
const timestampBegin = sendRequestTimestamp;

View File

@ -45,7 +45,7 @@ export function trimText(
while (startIndex <= stopIndex) {
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
const trimmedText =
currentIndex === maxIndex ? text : text.substr(0, currentIndex) + '…';
currentIndex === maxIndex ? text : text.slice(0, currentIndex) + '…';
if (getTextWidth(context, trimmedText) <= width) {
if (longestValidIndex < currentIndex) {

View File

@ -476,10 +476,10 @@ function processTimelineEvent(
break;
case 'blink.user_timing':
if (name.startsWith('--react-version-')) {
const [reactVersion] = name.substr(16).split('-');
const [reactVersion] = name.slice(16).split('-');
currentProfilerData.reactVersion = reactVersion;
} else if (name.startsWith('--profiler-version-')) {
const [versionString] = name.substr(19).split('-');
const [versionString] = name.slice(19).split('-');
profilerVersion = parseInt(versionString, 10);
if (profilerVersion !== SCHEDULING_PROFILER_VERSION) {
throw new InvalidProfileError(
@ -487,7 +487,7 @@ function processTimelineEvent(
);
}
} else if (name.startsWith('--react-lane-labels-')) {
const [laneLabelTuplesString] = name.substr(20).split('-');
const [laneLabelTuplesString] = name.slice(20).split('-');
updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
} else if (name.startsWith('--component-')) {
processReactComponentMeasure(
@ -497,7 +497,7 @@ function processTimelineEvent(
state,
);
} else if (name.startsWith('--schedule-render-')) {
const [laneBitmaskString] = name.substr(18).split('-');
const [laneBitmaskString] = name.slice(18).split('-');
currentProfilerData.schedulingEvents.push({
type: 'schedule-render',
@ -506,7 +506,7 @@ function processTimelineEvent(
warning: null,
});
} else if (name.startsWith('--schedule-forced-update-')) {
const [laneBitmaskString, componentName] = name.substr(25).split('-');
const [laneBitmaskString, componentName] = name.slice(25).split('-');
const forceUpdateEvent = {
type: 'schedule-force-update',
@ -524,7 +524,7 @@ function processTimelineEvent(
currentProfilerData.schedulingEvents.push(forceUpdateEvent);
} else if (name.startsWith('--schedule-state-update-')) {
const [laneBitmaskString, componentName] = name.substr(24).split('-');
const [laneBitmaskString, componentName] = name.slice(24).split('-');
const stateUpdateEvent = {
type: 'schedule-state-update',
@ -542,7 +542,7 @@ function processTimelineEvent(
currentProfilerData.schedulingEvents.push(stateUpdateEvent);
} else if (name.startsWith('--error-')) {
const [componentName, phase, message] = name.substr(8).split('-');
const [componentName, phase, message] = name.slice(8).split('-');
currentProfilerData.thrownErrors.push({
componentName,
@ -553,7 +553,7 @@ function processTimelineEvent(
});
} else if (name.startsWith('--suspense-suspend-')) {
const [id, componentName, phase, laneBitmaskString, promiseName] = name
.substr(19)
.slice(19)
.split('-');
const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
@ -604,7 +604,7 @@ function processTimelineEvent(
currentProfilerData.suspenseEvents.push(suspenseEvent);
state.unresolvedSuspenseEvents.set(id, suspenseEvent);
} else if (name.startsWith('--suspense-resolved-')) {
const [id] = name.substr(20).split('-');
const [id] = name.slice(20).split('-');
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
if (suspenseEvent != null) {
state.unresolvedSuspenseEvents.delete(id);
@ -613,7 +613,7 @@ function processTimelineEvent(
suspenseEvent.resolution = 'resolved';
}
} else if (name.startsWith('--suspense-rejected-')) {
const [id] = name.substr(20).split('-');
const [id] = name.slice(20).split('-');
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
if (suspenseEvent != null) {
state.unresolvedSuspenseEvents.delete(id);
@ -637,7 +637,7 @@ function processTimelineEvent(
state.potentialLongNestedUpdate = null;
}
const [laneBitmaskString] = name.substr(15).split('-');
const [laneBitmaskString] = name.slice(15).split('-');
throwIfIncomplete('render', state.measureStack);
if (getLastType(state.measureStack) !== 'render-idle') {
@ -682,7 +682,7 @@ function processTimelineEvent(
);
} else if (name.startsWith('--commit-start-')) {
state.nextRenderShouldGenerateNewBatchID = true;
const [laneBitmaskString] = name.substr(15).split('-');
const [laneBitmaskString] = name.slice(15).split('-');
markWorkStarted(
'commit',
@ -705,7 +705,7 @@ function processTimelineEvent(
state.measureStack,
);
} else if (name.startsWith('--layout-effects-start-')) {
const [laneBitmaskString] = name.substr(23).split('-');
const [laneBitmaskString] = name.slice(23).split('-');
markWorkStarted(
'layout-effects',
@ -722,7 +722,7 @@ function processTimelineEvent(
state.measureStack,
);
} else if (name.startsWith('--passive-effects-start-')) {
const [laneBitmaskString] = name.substr(24).split('-');
const [laneBitmaskString] = name.slice(24).split('-');
markWorkStarted(
'passive-effects',
@ -739,7 +739,7 @@ function processTimelineEvent(
state.measureStack,
);
} else if (name.startsWith('--react-internal-module-start-')) {
const stackFrameStart = name.substr(30);
const stackFrameStart = name.slice(30);
if (!state.internalModuleStackStringSet.has(stackFrameStart)) {
state.internalModuleStackStringSet.add(stackFrameStart);
@ -749,7 +749,7 @@ function processTimelineEvent(
state.internalModuleCurrentStackFrame = parsedStackFrameStart;
}
} else if (name.startsWith('--react-internal-module-stop-')) {
const stackFrameStop = name.substr(29);
const stackFrameStop = name.slice(29);
if (!state.internalModuleStackStringSet.has(stackFrameStop)) {
state.internalModuleStackStringSet.add(stackFrameStop);
@ -833,7 +833,7 @@ function processReactComponentMeasure(
state: ProcessorState,
): void {
if (name.startsWith('--component-render-start-')) {
const [componentName] = name.substr(25).split('-');
const [componentName] = name.slice(25).split('-');
assertNoOverlappingComponentMeasure(state);
@ -856,7 +856,7 @@ function processReactComponentMeasure(
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-layout-effect-mount-start-')) {
const [componentName] = name.substr(38).split('-');
const [componentName] = name.slice(38).split('-');
assertNoOverlappingComponentMeasure(state);
@ -879,7 +879,7 @@ function processReactComponentMeasure(
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-layout-effect-unmount-start-')) {
const [componentName] = name.substr(40).split('-');
const [componentName] = name.slice(40).split('-');
assertNoOverlappingComponentMeasure(state);
@ -902,7 +902,7 @@ function processReactComponentMeasure(
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-passive-effect-mount-start-')) {
const [componentName] = name.substr(39).split('-');
const [componentName] = name.slice(39).split('-');
assertNoOverlappingComponentMeasure(state);
@ -925,7 +925,7 @@ function processReactComponentMeasure(
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-passive-effect-unmount-start-')) {
const [componentName] = name.substr(41).split('-');
const [componentName] = name.slice(41).split('-');
assertNoOverlappingComponentMeasure(state);

View File

@ -26,7 +26,7 @@ export function formatDuration(ms: number): string {
export function trimString(string: string, length: number): string {
if (string.length > length) {
return `${string.substr(0, length - 1)}`;
return `${string.slice(0, length - 1)}`;
}
return string;
}

View File

@ -4,6 +4,47 @@
---
### 4.27.8
May 17, 2023
#### Bugfixes
* Fixed duplicated backend activation with multiple renderers ([hoxyq](https://github.com/hoxyq) in [#26807](https://github.com/facebook/react/pull/26807))
---
### 4.27.7
May 4, 2023
#### Bugfixes
* Fixed to work when browser devtools panel is reopened ([hoxyq](https://github.com/hoxyq) in [#26779](https://github.com/facebook/react/pull/26779))
* Fixed to work in Chrome incognito mode ([hoxyq](https://github.com/hoxyq) in [#26765](https://github.com/facebook/react/pull/26765))
---
### 4.27.6
April 20, 2023
#### Bugfixes
* Fixed backend injection logic for undocked devtools window ([mondaychen](https://github.com/mondaychen) in [#26665](https://github.com/facebook/react/pull/26665))
#### Other
* Use backend manager to support multiple backends in extension ([mondaychen](https://github.com/mondaychen) in [#26615](https://github.com/facebook/react/pull/26615))
---
### 4.27.5
April 17, 2023
#### Bugfixes
* Fixed copying to clipboard from context menu on Chrome ([hoxyq](https://github.com/hoxyq) in [#26604](https://github.com/facebook/react/pull/26604))
* Fixed external link behavior in the standalone app ([Willie-Boy](https://github.com/Willie-Boy) in [#26559](https://github.com/facebook/react/pull/26559))
#### Other
* Provide icon to edge devtools. ([harrygz889](https://github.com/harrygz889) in [#26543](https://github.com/facebook/react/pull/26543))
* Forbid editing class instances in props ([hoxyq](https://github.com/hoxyq) in [#26522](https://github.com/facebook/react/pull/26522))
* Improved script injection logic in browser extension ([mondaychen](https://github.com/mondaychen) in [#26492](https://github.com/facebook/react/pull/26492))
---
### 4.27.4
March 24, 2023

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools",
"version": "4.27.4",
"version": "4.27.8",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"repository": {
@ -27,7 +27,7 @@
"electron": "^23.1.2",
"ip": "^1.1.4",
"minimist": "^1.2.3",
"react-devtools-core": "4.27.4",
"react-devtools-core": "4.27.8",
"update-notifier": "^2.1.0"
}
}

View File

@ -7,34 +7,5 @@
* @flow
*/
export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
export const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
// Assumes there is no parent namespace.
export function getIntrinsicNamespace(type: string): string {
switch (type) {
case 'svg':
return SVG_NAMESPACE;
case 'math':
return MATH_NAMESPACE;
default:
return HTML_NAMESPACE;
}
}
export function getChildNamespace(
parentNamespace: string | null,
type: string,
): string {
if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
// No (or default) parent namespace: potential entry point.
return getIntrinsicNamespace(type);
}
if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
// We're leaving SVG.
return HTML_NAMESPACE;
}
// By default, pass namespace below.
return parentNamespace;
}

View File

@ -203,7 +203,7 @@ export function setValueForPropertyOnCustomComponent(
) {
if (name[0] === 'o' && name[1] === 'n') {
const useCapture = name.endsWith('Capture');
const eventName = name.substr(2, useCapture ? name.length - 9 : undefined);
const eventName = name.slice(2, useCapture ? name.length - 7 : undefined);
const prevProps = getFiberCurrentPropsFromNode(node);
const prevValue = prevProps != null ? prevProps[name] : null;

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