Compare commits

...

277 Commits
sim ... v17.0.2

Author SHA1 Message Date
Brian Vaughn
12adaffef7 Remove scheduler sampling profiler shared array buffer (#20840)
No one has been using this data so there's no reason to collect it. Event log has been maintained and tests have been updated.
2021-03-22 20:01:35 +00:00
Brian Vaughn
b2bbee7ba3 Disable (unstable) scheduler sampling profiler for OSS builds (#20832)
* Disabled Scheduler sampling profiler for OSS builds
* Added missing conditional feature flag around profiling calls
2021-03-22 19:57:29 +00:00
Toru Kobayashi
8cc6ff2488 fix: use SharedArrayBuffer only when cross-origin isolation is enabled (#20831)
* fix: check cross-origin isolation for SharedArrayBuffer

* chore: remove unused a $FlowFixMe comment

* prettier
2021-03-22 19:50:49 +00:00
Dan Abramov
8e5adfbd7e Remove usage of Array#fill (#20071) 2020-10-22 12:59:20 +01:00
Dan Abramov
89b610969d Bump versions for 17 2020-10-20 21:33:44 +01:00
Shivam Sandbhor
4ead6b5305 Treat <time> tag as a normal HTML tag. (#19951)
<time> tag has been supported by Chrome since Chrome 62.0.
Remove workarounds which were in place to avoid friction with
versions before Chrome 62.

Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
2020-10-06 03:15:32 +01:00
Brian Vaughn
1992d97306 Revert "Temporarily disable Profiler commit hooks flag (#19900)" (#19960) 2020-10-05 15:49:52 -04:00
Brian Vaughn
44d39c4d76 Removed skip-error-boundaries modifications from old fork (#19961)
Technically this change is unnecessary, since the feature is controlled by a flag, but since we decided not to ship this in v17– I'm going to remove it for now entirely.
2020-10-05 15:49:44 -04:00
Brian Vaughn
461cd84944 Revert "DevTools: Improve browser extension iframe support (#19854)" (#19959)
This reverts commit a99bf5c5f4.
2020-10-05 09:44:08 -04:00
Paul Doyle
cc77be957e Remove unnecessary error overriding in (#19949) 2020-10-02 22:10:46 +01:00
Brian Vaughn
97625272ab Debug tracing tests for CPU bound suspense (#19943) 2020-10-01 12:02:26 -04:00
Eugene Maslovich
43363e2795 Fix codestyle for typeof comparison (#19928) 2020-10-01 16:26:49 +01:00
John Wilson
8657ad4278 Fix(React DevTools) - prevent phishing attacks (#19934)
When a link opens a URL in a new tab with target="_blank", it is very simple for the opened page to change the location of the original page because the JavaScript variable window.opener is not null and thus "window.opener.location can be set by the opened page. This exposes the user to very simple phishing attacks.
2020-10-01 16:25:38 +01:00
Saikat Guha
91d2b6ef01 DevTools: Remove ReactJS.org version check "cheat" (#19939)
Remove dead code as facebook.github.io/react always redirects to reactjs.org, which has prod version of react. so removing the "cheat" (#19939)
2020-10-01 10:16:50 -04:00
Brian Vaughn
5427b4657b Temporarily disable Profiler commit hooks flag (#19900)
Temporarily disable Profiler commit hooks flag to verify it does not cause a regression.
2020-09-30 15:58:20 -04:00
Andrew Clark
1faf9e3dd5 Suspense for CPU-bound trees (#19936)
Adds a new prop to the Suspense component type,
`unstable_expectedLoadTime`. The presence of this prop indicates that
the content is computationally expensive to render.

During the initial mount, React will skip over expensive trees by
rendering a placeholder — just like we do with trees that are waiting
for data to resolve. That will help unblock the initial skeleton for the
new screen. Then we will continue rendering in the next commit.

For now, while we experiment with the API internally, any number passed
to `unstable_expectedLoadTime` will be treated as "computationally
expensive", no matter how large or small. So it's basically a boolean.
The reason it's a number is that, in the future, we may try to be clever
with this additional information. For example, SuspenseList could use
it as part of its heuristic to determine whether to keep rendering
additional rows.

Background
----------

Much of our early messaging and research into Suspense focused on its
ability to throttle the appearance of placeholder UIs. Our theory was
that, on a fast network, if everything loads quickly, excessive
placeholders will contribute to a janky user experience. This was backed
up by user research and has held up in practice.

However, our original demos made an even stronger assertion: not only is
it preferable to throttle successive loading states, but up to a certain
threshold, it’s also preferable to remain on the previous screen; or in
other words, to delay the transition.

This strategy has produced mixed results. We’ve found it works well for
certain transitions, but not for all them. When performing a full page
transition, showing an initial skeleton as soon as possible is crucial
to making the transition feel snappy. You still want throttle the nested
loading states as they pop in, but you need to show something on the new
route. Remaining on the previous screen can make the app feel
unresponsive.

That’s not to say that delaying the previous screen always leads to a
bad user experience. Especially if you can guarantee that the delay is
small enough that the user won’t notice it. This threshold is a called a
Just Noticeable Difference (JND). If we can stay under the JND, then
it’s worth skipping the first placeholder to reduce overall thrash.

Delays that are larger than the JND have some use cases, too. The main
one we’ve found is to refresh existing data, where it’s often preferable
to keep stale content on screen while the new data loads in the
background. It’s also useful as a fallback strategy if something
suspends unexpectedly, to avoid hiding parts of the UI that are already
visible.

We’re still in the process of optimizing our heuristics for the most
common patterns. In general, though, we are trending toward being more
aggressive about prioritizing the initial skeleton.

For example, Suspense is usually thought of as a feature for displaying
placeholders when the UI is missing data — that is, when rendering is
bound by pending IO.

But it turns out that the same principles apply to CPU-bound
transitions, too. It’s worth deferring a tree that’s slow to render if
doing so unblocks the rest of the transition — regardless of whether
it’s slow because of missing data or because of expensive CPU work.

We already take advantage of this idea in a few places, such as
hydration. Instead of hydrating server-rendered UI in a single pass,
React splits it into chunks. It can do this because the initial HTML
acts as its own placeholder. React can defer hydrating a chunk of UI as
long as it wants until the user interacts it. The boundary we use to
split the UI into chunks is the same one we use for IO-bound subtrees:
the <Suspense /> component.

SuspenseList does something similar. When streaming in a list of items,
it will occasionally stop to commit whatever items have already
finished, before continuing where it left off. It does this by showing a
placeholder for the remaining items, again using the same <Suspense />
component API, even if the item is CPU-bound.

Unresolved questions
--------------------

There is a concern that showing a placeholder without also loading new
data could be disorienting. Users are trained to believe that a
placeholder signals fresh content. So there are still some questions
we’ll need to resolve.
2020-09-30 12:57:33 -07:00
Brian Vaughn
7f08e908b1 Fix missing context to componentDidMount() when double-invoking lifecycles (#19935) 2020-09-30 15:56:19 -04:00
Brian Vaughn
9198a5cec0 Refactor layout effect methods (#19895)
Commit phase durations (layout and passive) are stored on the nearest (ancestor) Profiler and bubble up during the commit phase. This bubbling used to be implemented by traversing the return path each time we finished working on a Profiler to find the next nearest Profiler.

This commit removes that traversal. Instead, we maintain a stack of nearest Profiler ancestor while recursing the tree. This stack is maintained in the work loop (since that's where the recursive functions are) and so bubbling of durations has also been moved from commit-work to the work loop.

This PR also refactors the methods used to recurse and apply effects in preparation for the new Offscreen component type.
2020-09-29 15:58:20 -04:00
Andrew Clark
ba82eea383 Remove disableSchedulerTimeoutInWorkLoop flag (#19902)
We found and mitigated the root cause of the regression that led us to
temporarily revert this change. So now I'm un-reverting it.
2020-09-28 10:19:14 -07:00
Dmitriy Kovalenko
71bc8ac74c Improve wording for inline DevTools README.md (#19897) 2020-09-28 09:25:18 -04:00
Dan Abramov
480626a9e9 Create Synthetic Events Lazily (#19909) 2020-09-25 13:33:28 +01:00
Dan Abramov
0a00804494 Remove Array.from() from hot path (#19908)
* Remove Array.from() from hot path

* Fix build

Don't declare block variables inside loops
2020-09-25 11:33:07 +01:00
Dan Abramov
1890159a5d Separate SyntheticEvent constructors to prevent deopts (#19907)
* Remove arguments from hot path

* Make SyntheticEvent subtypes monomorphic

* Maybe fix Flow?
2020-09-25 11:31:00 +01:00
Luna Ruan
c63741fb3d offscreen double invoke effects (#19523)
This PR double invokes effects in __DEV__ mode.

We are thinking about unmounting layout and/or passive effects for a hidden tree. To understand potential issues with this, we want to double invoke effects. This PR changes the behavior in DEV when an effect runs from create() to create() -> destroy() -> create(). The effect cleanup function will still be called before the effect runs in both dev and prod. (Note: This change is purely for research for now as it is likely to break real code.)

**Note: The change is fully behind a flag and does not affect any of the code on npm.**
2020-09-24 13:42:17 -07:00
6h057
a99bf5c5f4 DevTools: Improve browser extension iframe support (#19854)
Co-authored-by: Joel DSouza <joel.dsouza@kapturecrm.com>
Co-authored-by: Damien Maillard <damien.maillard@dailymotion.com>
Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2020-09-23 16:56:13 -04:00
Brian Vaughn
c6917346ff Fixed broken Profiler test (#19894) 2020-09-23 15:33:25 -04:00
Brian Vaughn
87c023b1c1 Profiler onRender only called when we do work (#19885)
If we didn't perform any work in the subtree, skip calling onRender.
2020-09-22 14:47:13 -04:00
Todor Totev
92c7e49895 Don't consumer iterators while inspecting (#19831)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-09-22 14:23:20 -04:00
Andrew Clark
81aaee56af Don't call onCommit et al if there are no effects (#19863)
* Don't call onCommit et al if there are no effects

Checks `subtreeFlags` before scheduling an effect on the Profiler.

* Fix failing Profiler tests

The change to conditionally call Profiler commit hooks only if updates were scheduled broke a few of the Profiler tests. I've fixed the tests by either:
* Adding a no-op passive effect into the subtree or
* Converting onPostCommit to onCommit

When possible, I opted to add the no-op passive effect to the tests since that that hook is called later (during passive phase) so the test is a little broader. In a few cases, this required adding awkward act() wrappers so I opted to go with onCommit instead.

Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-09-22 11:20:26 -07:00
Andrew Clark
7355bf575a Consolidate commit phase hook functions (#19864)
There were a few pairs of commit phase functions that were almost
identical except for one detail. I've refactored them a bit to
consolidate their implementations:

- Lifted error handling logic when mounting a fiber's passive hook
effects to surround the entire list, instead of surrounding each effect.
- Lifted profiler duration tracking to surround the entire list.

In both cases, this matches the corresponding code for the layout phase.

The naming is still a bit of a mess but I'm not too concerned because
my next step is to refactor each commit sub-phase (layout, mutation)
so that we can store values on the JS stack. So the existing function
boundaries are about to change, anyway.
2020-09-22 11:16:27 -07:00
Dan Abramov
c91c1c4ebe Release script: allow preparing RC from npm 2020-09-22 14:27:50 +01:00
E-Liang Tan
04e21efd09 Add scheduling profiler deployment CI job (#19874)
* Add vercel to scheduling profiler dev deps
* Add vercel.json
* Add CD job
* Add CD setup instructions
2020-09-22 08:34:06 -04:00
Brian Vaughn
ded2a83ebf Improved DevTools context test harness (#19878) 2020-09-21 16:39:53 -04:00
E-Liang Tan
6d73063ddf Enable building of DevTools and scheduling profiler in CI (#19691)
Re-enables building of main DevTools in CI and add new CI target for building the scheduling profiler.
2020-09-21 11:49:07 -04:00
Brian Vaughn
a8de69f358 DevTools: Drop IE 11 support (#19875)
DevTools shared Babel config previously supported IE 11 to target Hermes (for the standalone backend that gets embedded within React Native apps). This targeting resulted in less optimal code for other DevTools targets though which did not need to support IE 11. This PR updates the shared config to remove IE 11 support by default, and only enables it for the standalone backend target.
2020-09-21 11:07:45 -04:00
Dan Abramov
bc6b7b6b16 Don't trigger lazy in DEV during element creation (#19871) 2020-09-21 16:04:49 +01:00
Gustavo Saiani
a774502e0f Use single quotes in getComponentName return (#19873) 2020-09-21 13:35:21 +01:00
Andrew Clark
8b2d3783e5 Use Passive flag to schedule onPostCommit (#19862)
Instead of calling `onPostCommit` in a separate phase, we can fire
them during the same traversal as the rest of the passive effects.

This works because effects are executed depth-first. So by the time we
reach a Profiler node, we'll have already executed all the effects in
its subtree.
2020-09-18 13:02:26 -07:00
Brian Vaughn
50d9451f32 Improve DevTools editing interface (#19774)
* Improve DevTools editing interface

This commit adds the ability to rename or delete keys in the props/state/hooks/context editor and adds tests to cover this functionality. DevTools will degrade gracefully for older versions of React that do not inject the new reconciler rename* or delete* methods.

Specifically, this commit includes the following changes:
* Adds unit tests (for modern and legacy renderers) to cover overriding props, renaming keys, and deleting keys.
* Refactor backend override methods to reduce redundant Bridge/Agent listeners and methods.
* Inject new (DEV-only) methods from reconciler into DevTools to rename and delete paths.
* Refactor 'inspected element' UI components to improve readability.
* Improve auto-size input to better mimic Chrome's Style editor panel. (See this Code Sandbox for a proof of concept.)

It also contains the following code cleanup:
* Additional unit tests have been added for modifying values as well as renaming or deleting paths.
* Four new DEV-only methods have been added to the reconciler to be injected into the DevTools hook: overrideHookStateDeletePath, overrideHookStateRenamePath, overridePropsDeletePath, and overridePropsRenamePath. (DevTools will degrade gracefully for older renderers without these methods.)
* I also took this as an opportunity to refactor some of the existing code in a few places:
  * Rather than the backend implementing separate methods for editing props, state, hooks, and context– there are now three methods: deletePath, renamePath, and overrideValueAtPath that accept a type argument to differentiate between props, state, context, or hooks.
  * The various UI components for the DevTools frontend have been refactored to remove some unnecessary repetition.

This commit also adds temporary support for override* commands with mismatched backend/frontend versions:
* Add message forwarding for older backend methods (overrideContext, overrideHookState, overrideProps, and overrideState) to the new overrideValueAtPath method. This was done in both the frontend Bridge (for newer frontends passing messages to older embedded backends) and in the backend Agent (for older frontends passing messages to newer backends). We do this because React Native embeds the React DevTools backend, but cannot control which version of the frontend users use.
* Additional unit tests have been added as well to cover the older frontend to newer backend case. Our DevTools test infra does not make it easy to write tests for the other way around.
2020-09-18 11:07:18 -04:00
Johnny Pribyl
b3b1bb9ce2 Enable source maps for DevTools production builds (#19773)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-09-18 10:07:22 -04:00
Brian Vaughn
26857ecfa9 Revert "DevTools: Improve browser extension iframe support (#19827)" (#19852)
This reverts commit ec39a5e901.
2020-09-17 14:57:44 -04:00
Dan Abramov
6fddca27e7 Remove passive intervention flag (#19849) 2020-09-17 15:37:12 +01:00
Adnaan Bheda
36df9185c5 chore(docs): Removed outdated comment about fb.me link (#19830) 2020-09-16 08:07:16 -04:00
Brian Vaughn
16fb2b6f9e Moved resetChildLanes into complete work (#19836)
This allows us to inline a few checks that are specific to a certain tag-type.
2020-09-15 17:12:48 -04:00
Andrew Clark
b93f3e7d2d Fix act bundle size regression (#19832)
Adds back the `TestUtils.act` implementation that I had removed
in #19745. This version of `act` is implemented in "userspace" (i.e. not
the reconciler), so it doesn't add to the production bundle size.

I had removed this in #19745 in favor of the `act` exported by the
reconciler because I thought we would remove support for `act` in
production in the impending major release. (It currently warns.)

However, we've since decided to continue supporting `act` in prod for
now, so that it doesn't block people from upgrading to v17. We'll drop
support in a future major release.

So, to avoid bloating the production bundle size, we need to move the
public version of `act` back to "userspace", like it was before.

This doesn't negate the main goal of #19745, though, which was to
decouple the public version(s) of `act` from the internal one that we
use to test React itself.
2020-09-14 10:11:47 -07:00
6h057
ec39a5e901 DevTools: Improve browser extension iframe support (#19827)
Co-authored-by: Joel DSouza <joel.dsouza@kapturecrm.com>
Co-authored-by: Damien Maillard <damien.maillard@dailymotion.com>
Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2020-09-14 10:04:46 -04:00
6h057
917cb01a58 React DevTools: Show symbols used as keys in state (#19786)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-09-14 09:55:19 -04:00
Dan Abramov
11ee82df45 [Events] Make passiveness and priority non-configurable (#19807) 2020-09-14 13:54:08 +01:00
Seth Webster
ebb2253428 updates mailmap entries (#19824) 2020-09-12 13:05:52 -04:00
Dan Abramov
cc581065df eslint-plugin-react-hooks@4.1.2 2020-09-11 13:18:44 +01:00
Dan Abramov
0044805c88 Update CHANGELOG.md 2020-09-11 13:17:55 +01:00
Sebastian Silbermann
0f70d4dd66 Consider components in jsx as missing dependencies in @typescript-eslint/parser@4.x (#19815)
* Run JS tests with TS esling parser

* Add failing test

* fix: Mark JSXIdentifier has missing dependency

* Safe isSameIdentifier
2020-09-11 13:13:43 +01:00
Andrew Clark
84558c61ba Don't visit passive effects during layout phase (#19809)
Removes the `Update` flag when scheduling a passive effect for
`useEffect`. The `Passive` flag alone is sufficient.

This doesn't affect any behavior, but does optimize the performance of
the commit phase.
2020-09-10 14:00:41 -07:00
Dan Abramov
ad8a0a8cd0 eslint-plugin-react-hooks@4.1.1 2020-09-10 19:54:42 +01:00
Dan Abramov
77544a0d6f Update CHANGELOG.md 2020-09-10 19:54:10 +01:00
Sebastian Silbermann
ed4fdfc737 test(eslint-plugin-react-hooks): Run with TS parsers >= 2.x (#19792)
* test(eslint-plugin-react-hooks): Run with TS parsers >= 2.x

* name test suites for each parser
2020-09-10 19:08:47 +01:00
Matthias Schiffer
cd75f93c03 eslint-plugin-react-hooks: fix compatibility with @typescript-eslint/parser@4.0.0+ (#19751)
In addition to `TSTypeQuery`, dependency nodes with a `TSTypeReference`
parent need to be ignored as well. Without this fix, generic type
variables will be listed as missing dependencies.

Example:

    export function useFoo<T>(): (foo: T) => boolean {
        return useCallback((foo: T) => false, []);
    }

This will report the following issue:

    React Hook useCallback has a missing dependency: 'T'. Either include
    it or remove the dependency array

Closes: #19742
2020-09-10 11:30:18 +01:00
Dan Abramov
a08ae9f147 Listen to onScroll during hydration (#19803) 2020-09-10 11:00:02 +01:00
Gustavo Saiani
781212aab3 Remove double space in test name (#19762) 2020-09-09 18:26:44 -04:00
Andrew Clark
e7b255341b Internal act: Flush timers at end of scope (#19788)
If there are any suspended fallbacks at the end of the `act` scope,
force them to display by running the pending timers (i.e. `setTimeout`).

The public implementation of `act` achieves the same behavior with an
extra check in the work loop (`shouldForceFlushFallbacks`). Since our
internal `act` needs to work in both development and production, without
additional runtime checks, we instead rely on Jest's mock timers.

This doesn't not affect refresh transitions, which are meant to delay
indefinitely, because in that case we exit the work loop without
posting a timer.
2020-09-08 21:55:23 -07:00
Andrew Clark
d17086c7c8 Decouple public, internal act implementation (#19745)
In the next major release, we intend to drop support for using the `act`
testing helper in production. (It already fires a warning.) The
rationale is that, in order for `act` to work, you must either mock the
testing environment or add extra logic at runtime. Mocking the testing
environment isn't ideal because it requires extra set up for the user.
Extra logic at runtime is fine only in development mode — we don't want
to slow down the production builds.

Since most people only run their tests in development mode, dropping
support for production should be fine; if there's demand, we can add it
back later using a special testing build that is identical to the
production build except for the additional testing logic.

One blocker for removing production support is that we currently use
`act` to test React itself. We must test React in both development and
production modes.

So, the solution is to fork `act` into separate public and
internal implementations:

- *public implementation of `act`* – exposed to users, only works in
  development mode, uses special runtime logic, does not support partial
  rendering
- *internal implementation of `act`* – private, works in both
  development and productionm modes, only used by the React Core test
  suite, uses no special runtime logic, supports partial rendering (i.e.
  `toFlushAndYieldThrough`)

The internal implementation should mostly match the public
implementation's behavior, but since it's a private API, it doesn't have
to match exactly. It works by mocking the test environment: it uses a
mock build of Scheduler to flush rendering tasks, and Jest's mock timers
to flush Suspense placeholders.

---

In this first commit, I've added the internal forks of `act` and
migrated our tests to use them. The public `act` implementation is
unaffected for now; I will leave refactoring/clean-up for a later step.
2020-09-08 08:11:45 -07:00
Joseph Savona
d38ec17b1d [Flight] Set dispatcher for duration of performWork() (#19776) 2020-09-07 19:41:22 -04:00
Andrew Clark
4f3f7eeb7f Bugfix: Effect clean up when deleting suspended tree (#19752)
* Bug: Effect clean up when deleting suspended tree

Adds a failing unit test.

* Re-use static flags from suspended primary tree

When switching to a Suspense boundary's fallback, we need to be sure
to preserve static subtree flags from the primary tree.
2020-09-04 15:46:44 -07:00
Andrew Clark
7baf9d4128 Combine Flags and SubtreeFlags types (#19775)
Because the `subtreeFlags` is the union of all the flags present in
a subtree, we can use the same type as `flags`.

One practical benefit is that we can bubble up the flags from the
children with a single `|=` operator.

Structurally, everything else about the effect algorithm is unchanged.
2020-09-04 14:48:30 -07:00
Andrew Clark
1665443603 Rename effect fields (#19755)
- `effectTag` -> `flags`
- `subtreeTag` -> `subtreeFlags`
2020-09-04 14:34:07 -07:00
Andrew Clark
708fa77a78 Decrease expiration time of input updates (#19772)
Changes the expiration time of input updates from 1000ms to 250ms, to
match the corresponding constant in Scheduler.js.

When we made it larger, a product metric in www regressed, suggesting
there's a user interaction that's being starved by a series of
synchronous updates. If that theory is correct, the proper solution is
to fix the starvation. However, this scenario supports the idea that
expiration times are an important safeguard when starvation does happen.

Also note that, in the case of user input specifically, this will soon
no longer be an issue because we plan to make user input synchronous by
default (until you enter `startTransition`, of course.)

If weren't planning to make these updates synchronous soon anyway, I
would probably make this number a configurable parameter.
2020-09-04 10:42:09 -07:00
Ricky
36df483af4 Add feature flag to disable scheduler timeout in work loop (#19771) 2020-09-04 10:58:17 -04:00
E-Liang Tan
eabd18c73f Scheduling Profiler: Move preprocessing to web worker and add loading indicator (#19759)
* Move preprocessData into a web worker
* Add UI feedback for loading/import error states
* Terminate worker when done handling profile
* Add display density CSS variables
2020-09-04 10:57:32 -04:00
E-Liang Tan
38a512acad Scheduling Profiler: Redesign with DevTools styling (#19707)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-09-03 12:08:40 -04:00
Dan Abramov
bcc0aa4633 Revert "Revert "Remove onScroll bubbling flag (#19535)" (#19655)" (#19761)
This reverts commit 64ddef44c6.
2020-09-03 17:06:20 +01:00
E-Liang Tan
e9454894ff Scheduling Profiler: Extract and test scroll state from horizontal pan and zoom view (#19682)
* Extract reusable scroll logic from HorizontalPanAndZoomView

* Change VerticalScrollView to use scrollState

* Clarify test name
2020-09-03 10:27:35 -04:00
E-Liang Tan
9340083395 Scheduling Profiler: Add Fast Refresh (#19757) 2020-09-03 09:17:57 -04:00
Meghnath Pillay
835c11eba7 Add checkbox toggle for boolean values (#19714)
* added a checkbox which appears to the right of a value when value is boolean
* checkbox with toggle capability created for boolean props

Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2020-09-03 08:57:12 -04:00
Brian Vaughn
99cae887f3 Add failing test for passive effect cleanup functions and memoized components (#19750)
* Add failing tests for passive effects cleanup not being called for memoized components

* Bubble passive static subtreeTag even after bailout

This prevents subsequent unmounts from skipping over any pending passive effect destroy functions
2020-09-02 13:01:52 -04:00
Ikko Ashimine
2cfd73c4d0 Fix typo in comment (Noticable→Noticeable) (#19737) 2020-09-01 20:19:10 -04:00
Brian Vaughn
98dba66ee1 Add ⎇ + arrow key navigation to DevTools (#19741)
⎇ + left/right navigates between owners (similar to owners tree) and ⎇ + up/down navigations between siblings.
2020-09-01 20:03:44 -04:00
Bhumij Gupta
53e622ca7f Fix instances of function declaration after return (#19733)
* Add ESLint plugin to check for any function declare after return
* Refactor code to move function declarations before return and fix failing lint
2020-09-01 08:55:10 -04:00
Kevin Weber
b7d18c4daf Support Babel's envName option in React Refresh plugin (#19009)
* Fix envName bug

* Replace getEnv with env
2020-09-01 13:50:54 +01:00
Andrew Clark
1f38dcff67 Remove withSuspenseConfig (#19724)
Removes `withSuspenseConfig` and migrates relevant tests to
`startTransition` instead.

We only had one caller in www, which I've removed.
2020-08-31 09:16:49 -07:00
Pascal Fong Kye
1396e4a8f5 Fixes eslint warning when node type is ChainExpression (#19680)
* Add babel parser which supports ChainExpression

* Add and fix tests for new babel eslint parser

* extract function to mark node

* refactor for compatibility with eslint v7.7.0+

* Update eslint to v7.7.0
Update hook test since eslint now supports nullish coalescing
2020-08-29 21:03:23 +01:00
Andrew Clark
a8500be893 Add startTransition as a known stable method (#19720)
The `startTransition` method returned from `useTransition` is a stable
method, like `dispatch` or `setState`. You should not have to specify
it as a hook dependency.
2020-08-28 16:44:39 -07:00
Andrew Clark
4f5fb56100 Use gate pragma instead of if (__EXPERIMENTAL__) (#19722)
* Use gate pragma instead of if (__EXPERIMENTAL__)

* Fix stream error handling in tests

Added an error listener so that the tests fail within their Jest scope,
instead of crashing the whole process.
2020-08-28 14:21:01 -07:00
Timothy Yung
380dc95de8 Revert "Append text string to <Text> error message (#19581)" (#19723)
This reverts commit 1a41a196bc.
2020-08-28 13:46:59 -07:00
Andrew Clark
ddd1faa197 Remove config argument from useTransition (#19719)
And `useDeferredValue`.

The options were already disabled in previous commits, so this doesn't
change any behavior. I upated type signatures and cleaned up the hook
implementation a bit — no longer have to wrap the `start` method with
`useCallback`, because its only remaining dependency is a `setState`
method, which never changes. Instead, we can store the `start` method
on a ref.
2020-08-28 11:49:01 -07:00
Andrew Clark
92fcd46cc7 Replace SuspenseConfig object with an integer (#19706)
Now that the options in SuspenseConfig are no longer supported, the
only thing we use it for is to track whether an update is part of
a transition.

I've renamed `ReactCurrentBatchConfig.suspense` to
`ReactCurrentBatchConfig.transition`, and changed the type to a number.
The number is always either 0 or 1. I could have made it a boolean;
however, most likely this will eventually be either a Lane or an
incrementing identifier.

The `withSuspenseConfig` export still exists until we've removed
all the callers from www.
2020-08-28 10:52:56 -07:00
Dan Abramov
b754caaaf2 Enable eager listeners in open source (#19716)
* Enable eager listeners in open source

* Fix tests

* Enable in all places
2020-08-28 12:23:28 +01:00
Dan Abramov
c1ac052158 [Flight] Support more element types and Hooks for Server and Hybrid Components (#19711)
* Shim support for more element types

* Shim commonly used Hooks that are safe

* Flow

* Oopsie
2020-08-27 20:19:13 +01:00
Andrew Clark
1eaafc9ade Clean up timeoutMs-related implementation details (#19704)
* Disable busyDelayMs and busyMinDurationMs

Refer to explanation in previous commit.

* Remove unnecessary work loop variables

Since we no longer support SuspenseConfig options, we don't need to
track these values.

* Remove unnecessary Update fields
2020-08-27 09:47:58 -07:00
Andrew Clark
8da0da0937 Disable timeoutMs argument (#19703)
* Remove distinction between long, short transitions

We're removing the `timeoutMs` option, so there's no longer any
distinction between "short" and "long" transitions. They're all treated
the same.

This commit doesn't remove `timeoutMs` yet, only combines the internal
priority levels.

* Disable `timeoutMs` argument

tl;dr
-----

- We're removing the `timeoutMs` argument from `useTransition`.
- Transitions will either immediately switch to a skeleton/placeholder
  view (when loading new content) or wait indefinitely until the data
  resolves (when refreshing stale content).
- This commit disables the `timeoutMS` so that the API has the desired
  semantics. It doesn't yet update the types or migrate all the test
  callers. I'll do those steps in follow-up PRs.

Motivation
----------

Currently, transitions initiated by `startTransition` / `useTransition`
accept a `timeoutMs` option. You can use this to control the maximum
amount of time that a transition is allowed to delay before we give up
and show a placeholder.

What we've discovered is that, in practice, every transition falls into
one of two categories: a **load** or a **refresh**:

- **Loading a new screen**: show the next screen as soon as possible,
  even if the data hasn't finished loading. Use a skeleton/placeholder
  UI to show progress.
- **Refreshing a screen that's already visible**: keep showing the
  current screen indefinitely, for as long as it takes to load the fresh
  data, even if the current data is stale. Use a pending state (and
  maybe a busy indicator) to show progress.

In other words, transitions should either *delay indefinitely* (for a
refresh) or they should show a placeholder *instantly* (for a load).
There's not much use for transitions that are delayed for a
small-but-noticeable amount of time.

So, the plan is to remove the `timeoutMs` option. Instead, we'll assign
an effective timeout of `0` for loads, and `Infinity` for refreshes.

The mechanism for distinguishing a load from a refresh already exists in
the current model. If a component suspends, and the nearest Suspense
boundary hasn't already mounted, we treat that as a load, because
there's nothing on the screen. However, if the nearest boundary is
mounted, we treat that as a refresh, since it's already showing content.

If you need to fix a transition to be treated as a load instead of a
refresh, or vice versa, the solution will involve rearranging the
location of your Suspense boundaries. It may also involve adding a key.

We're still working on proper documentation for these patterns. In the
meantime, please reach out to us if you run into problems that you're
unsure how to fix.

We will remove `timeoutMs` from `useDeferredValue`, too, and apply the
same load versus refresh semantics to the update that spawns the
deferred value.

Note that there are other types of delays that are not related to
transitions; for example, we will still throttle the appearance of
nested placeholders (we refer to this as the placeholder "train model"),
and we may still apply a Just Noticeable Difference heuristic (JND) in
some cases. These aren't going anywhere. (Well, the JND heuristic might
but for different reasons than those discussed above.)
2020-08-26 14:35:13 -07:00
Ben Pernick
60ba723bf7 Add SuspenseList to devTools (#19684)
* ensure getDisplayName is only called on functions

* add SuspenseList to Dev tools element names

* Add SuspenseList and pass tests

* Import SuspenseList directly

* run prettier

* Refactor tests to use real components

* run linter
2020-08-26 18:04:43 +01:00
Ricky
5564f2c95b Add React.startTransition (#19696)
* Add React.startTransition

* Export startTransition from index.js as well
2020-08-26 11:34:17 -04:00
inottn
c4e0768d74 Remove unused argument from finishConcurrentRender (#19689) 2020-08-25 13:49:57 +01:00
Andrew Clark
af219cc6e6 Lint rule to forbid access of cross-fork fields (#19679)
* Lint rule to forbid access of cross-fork fields

We use a shared Fiber type for both reconciler forks (old and new). It
is a superset of all the fields used by both forks. However, there are
some fields that should only be used in the new fork, and others that
should only be used in the old fork.

Ideally we would enforce this with separate Flow types for each fork.
The problem is that the Fiber type is accessed by some packages outside
the reconciler (like React DOM), and get passed into the reconciler as
arguments. So there's no way to fork the Fiber type without also forking
the packages where they are used. FiberRoot has the same issue.

Instead, I've added a lint rule that forbids cross-fork access of
fork-specific fields. Fields that end in `_old` or `_new` are forbidden
from being used inside the new or old fork respectively. Or you can
specific custom fields using the ESLint plugin options.

I used this plugin to find and remove references to the effect list
in d2e914a.

* Mark effect list fields as old

And `subtreeTag` as new.

I didn't mark `lastEffect` because that name is also used by the
Hook type. Not super important; could rename to `lastEffect_old` but
idk if it's worth the effort.
2020-08-24 10:07:11 -07:00
Dan Abramov
848bb2426e Attach Listeners Eagerly to Roots and Portal Containers (#19659)
* Failing test for #19608

* Attach Listeners Eagerly to Roots and Portal Containers

* Forbid createEventHandle with custom events

We can't support this without adding more complexity. It's not clear that this is even desirable, as none of our existing use cases need custom events. This API primarily exists as a deprecation strategy for Flare, so I don't think it is important to expand its support beyond what Flare replacement code currently needs. We can later revisit it with a better understanding of the eager/lazy tradeoff but for now let's remove the inconsistency.

* Reduce risk by changing condition only under the flag

Co-authored-by: koba04 <koba0004@gmail.com>
2020-08-24 16:50:20 +01:00
Dominic Gannaway
8c9fc4e90f Remove usage of PossiblyWeakSet from createEventHandle (#19686) 2020-08-24 16:10:23 +01:00
Toru Kobayashi
2ada4bd0c2 Add a test for non-passive event handlers for events affected by the browsers' intervention (#19658) 2020-08-24 14:30:17 +01:00
Andrew Clark
d2e914ab4e Remove remaining references to effect list (#19673)
* Remove `firstEffect` null check

This is the last remaining place where the effect list has semantic
implications.

I've replaced it with a check of `effectTag` and `subtreeTag`, to see
if there are any effects in the whole tree. This matches the semantics
of the old check. However, I think only reason this optimization exists
is because it affects profiling. We should reconsider whether this
is necessary.

* Remove remaining references to effect list

We no longer use the effect list anywhere in our implementation. It's
been replaced by a recursive traversal in the commit phase.

This removes all references to the effect list in the new fork.
2020-08-21 20:42:32 -05:00
Brian Vaughn
0386bd0da2 Tweaked landing page styles (#19665)
* Tweaked landing page styles

* Update GitHub link

Co-authored-by: E-Liang Tan <eliang@eliangtan.com>
2020-08-21 14:06:01 -04:00
Dan Abramov
90d212d326 Fix movementX/Y polyfill with capture events (#19672)
* Fix movementX/Y polyfill with capture events

* Remove unnecesary call for better inlining
2020-08-21 16:55:41 +01:00
6h057
49af88991c Fix DevTools crash when inspecting document.all (#19619)
* Add html_all_collection type to correct typeof document.all

* process HTMLAllCollection like HTMLElement + fix flow issue

* fix lint

* move flow fix comment

* Make it work with iframes too

* optimize how we get html_all_collection type

* use once Object.prototype.toString.call
2020-08-21 14:31:16 +01:00
Sebastian Markbåge
d6e433899f Use Global Render Timeout for CPU Suspense (#19643)
* Use Retry lane for resuming CPU suspended work

* Use a global render timeout for CPU suspense heuristics

* Fix profiler test since we're now reading time more often

* Sync to new reconciler

* Test synchronously rerendering should not render more rows
2020-08-20 14:39:29 -07:00
Hemakshi Sachdev
f912186012 Fix: Unspecified error for IE11 (#19530) (#19664) 2020-08-20 21:53:23 +01:00
Brian Vaughn
d5ed78764f Visually distinguish user timing marks from React events (#19663) 2020-08-20 14:43:40 -04:00
E-Liang Tan
2bea3fb0b8 Import React Concurrent Mode Profiler (#19634)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
Co-authored-by: Kartik Choudhary <kartikc.918@gmail.com>
2020-08-20 14:06:28 -04:00
E-Liang Tan
c641b611c4 Upgrade all @babel/* packages to fix DevTools builds on newer Node versions (#19647) 2020-08-20 11:47:16 -04:00
Dan Abramov
08e69f65b4 Fix incorrect copy-paste in test (#19657) 2020-08-20 01:47:32 +01:00
Dan Abramov
64ddef44c6 Revert "Remove onScroll bubbling flag (#19535)" (#19655)
This reverts commit e9721e14e4.
2020-08-19 20:54:54 +01:00
Dan Abramov
dd651df05e Keep onTouchStart, onTouchMove, and onWheel passive (#19654)
* Keep onTouchStart, onTouchMove, and onWheel passive

* Put it behind a feature flag on WWW
2020-08-19 18:42:33 +01:00
Sebastian Silbermann
87b3e2d257 Add flow to SyntheticEvent (#19564)
* Add flow to SyntheticEvent

* Minimal implementation of known and unknown synthetic events

* less casting

* Update EnterLeaveEventPlugin.js

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2020-08-19 14:16:48 +01:00
Julien Gilli
b8fa09e9e2 provide profiling bundle for react-reconciler (#19559) 2020-08-19 14:11:27 +01:00
Toru Kobayashi
32ca313060 refactor: remove unnecessary return statements (#19625) 2020-08-19 14:10:41 +01:00
Sam Marks
c45a195429 fix: property list values should show whitespace (#19640) 2020-08-19 08:50:46 -04:00
Brian Vaughn
23595ff593 Add missing param to safelyCallDestroy() (#19638) 2020-08-18 12:02:04 -04:00
ᴜɴвʏтᴇ
24f1923b1b fix: open two same tabs on firefox (#19632)
close #19629
2020-08-18 10:17:00 -04:00
Luna Ruan
ee409ea3b5 change destroy to safelyCallDestroy (#19605)
We use safelyCallDestroy for commitUnmount and passive effects unmounts but we call destroy directly in commitHookEffectListUnmount (AKA layout effects unmounts because we don't use this anywhere else). This PR changes the direct destroy call to safelyCallDestroy for consistency
2020-08-17 17:32:50 -07:00
Brian Vaughn
bcca5a6ca7 Always skip unmounted/unmounting error boundaries (#19627)
The behavior of error boundaries for passive effects that throw during cleanup was recently changed so that React ignores boundaries which are also unmounting in favor of still-mounted boundaries. This commit implements that same behavior for layout effects (useLayoutEffect, componentWillUnmount, and ref-detachment).

The new, skip-unmounting-boundaries behavior is behind a feature flag (`skipUnmountedBoundaries`).
2020-08-17 15:01:06 -04:00
Timothy Yung
1a41a196bc Append text string to <Text> error message (#19581)
* Append text string to <Text> error message

* Truncate text in <Text> error message

* Regenerate `codes.json`
2020-08-17 10:47:49 -07:00
Jack Works
1287670191 Fix: React cannot render in ShadowRoot (#15894)
* fix: render in shadow root

* fix: flow typing

* Remove types and turn invariant into warning

Co-authored-by: Dan Abramov <dan.abramov@me.com>
2020-08-17 15:47:49 +01:00
Dan Abramov
e4afb2fddf eslint-plugin-react-hooks@4.1.0 2020-08-17 13:31:20 +01:00
Dan Abramov
ced05c46c1 Update CHANGELOG.md 2020-08-17 13:30:16 +01:00
CY Lim
702fad4b1b refactor fb.me redirect link to reactjs.org/link (#19598)
* refactor fb.me url to reactjs.org/link

* Update ESLintRuleExhaustiveDeps-test.js

* Update ReactDOMServerIntegrationUntrustedURL-test.internal.js

* Update createReactClassIntegration-test.js

* Update ReactDOMServerIntegrationUntrustedURL-test.internal.js

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2020-08-17 13:25:50 +01:00
Toru Kobayashi
49cd77d24a fix: leak strict mode with UMD builds (#19614) 2020-08-15 14:42:49 +01:00
Brian Vaughn
ffb749c95e Improve error boundary handling for unmounted subtrees (#19542)
A passive effect's cleanup function may throw after an unmount. Prior to this commit, such an error would be ignored. (React would not notify any error boundaries.) After this commit, React's behavior varies depending on which reconciler fork is being used.

For the old reconciler, React will call componentDidCatch for the nearest unmounted error boundary (if there is one). If there are no unmounted error boundaries, React will still swallow the error because the return pointer has been disconnected, so the normal error handling logic does not know how to traverse the tree to find the nearest still-mounted ancestor.

For the new reconciler, React will skip any unmounted boundaries and look for a still-mounted boundary. If one is found, it will call getDerivedStateFromError and/or componentDidCatch (depending on the type of boundary).

Tests have been added for both reconciler variants for now.
2020-08-14 16:46:46 -04:00
Brian Vaughn
9b35dd2fcc Permanently removed component stacks from scheduling profiler data (#19615)
These stacks improve the profiler data but they're expensive to generate and generating them can also cause runtime errors in larger applications (although an exact repro has been hard to nail down). Removing them for now. We can revisit adding them after this profiler has been integrated into the DevTools extension and we can generate them lazily.
2020-08-14 15:21:13 -04:00
Andrew Clark
3f8115cdd1 Remove didTimeout check from work loop
No longer need this, since we have starvation protection in userspace.

This will also allow us to remove the concept from the Scheduler
package, which is nice because `postTask` doesn't currently support it.
2020-08-14 10:45:24 -04:00
Andrew Clark
9abc2785cb Remove wasteful checks from shouldYield
`shouldYield` will currently return `true` if there's a higher priority
task in the Scheduler queue.

Since we yield every 5ms anyway, this doesn't really have any practical
benefit. On the contrary, the extra checks on every `shouldYield` call
are wasteful.
2020-08-14 10:45:11 -04:00
Clay Tercek
fe6d05229f fix event.relatedTarget fallback logic for firefox (#19607)
* fix event.relatedTarget fallback logic for firefox

* check if relatedTarget is undefined for fallback
2020-08-14 13:05:53 +01:00
Jordan Eldredge
1d5e10f703 [eslint-plugin-react-hooks] Report constant constructions (#19590)
* [eslint-plugin-react-cooks] Report constant constructions

The dependency array passed to a React hook can be thought of as a list of cache keys. On each render, if any dependency is not `===` its previous value, the hook will be rerun. Constructing a new object/array/function/etc directly within your render function means that the value will be referentially unique on each render. If you then use that value as a hook dependency, that hook will get a "cache miss" on every render, making the dependency array useless.

This can be especially dangerous since it can cascade. If a hook such as `useMemo` is rerun on each render, not only are we bypassing the option to avoid potentially expensive work, but the value _returned_ by `useMemo` may end up being referentially unique on each render causing other downstream hooks or memoized components to become deoptimized.

* Fix/remove existing tests

* Don't give an autofix of wrapping object declarations

It may not be safe to just wrap the declaration of an object, since the object may get mutated.

Only offer this autofix for functions which are unlikely to get mutated.

Also, update the message to clarify that the entire construction of the value should get wrapped.

* Handle the long tail of nodes that will be referentially unique

* Catch let/var constant constructions on initial assignment

* Trim trailing whitespace

* Address feedback from @gaearon

* Rename "assignment" to "initialization"

* Add test for a constant construction used in multiple dependency arrays
2020-08-13 20:54:33 +01:00
Sebastian Markbåge
dab0854c5e Move commit passive unmount/mount to CommitWork (#19599) 2020-08-13 09:17:33 -07:00
Brian Vaughn
c3ee973c56 Fix emoji character displayed in Chrome extension (#19603) 2020-08-13 11:16:35 -04:00
inottn
ccb6c39451 Remove unused argument (#19600) 2020-08-13 15:11:01 +01:00
Andrew Clark
629125555f [Scheduler] Re-throw unhandled errors (#19595)
Because `postTask` returns a promise, errors inside a `postTask`
callback result in the promise being rejected.

If we don't catch those errors, then the browser will report an
"Unhandled promise rejection" error. This is a confusing message to see
in the console, because the fact that `postTask` is a promise-based API
is an implementation detail from the perspective of the developer.
"Promise rejection" is a red herring.

On the other hand, if we do catch those errors, then we need to report
the error to the user in some other way.

What we really want is the default error reporting behavior that a
normal, non-Promise browser event gets.

So, we'll re-throw inside `setTimeout`.
2020-08-12 20:06:43 -07:00
Pascal Fong Kye
b6e1d08604 DevTools bug fix: Proxied methods should be safely dehydrated for display 2020-08-12 12:15:53 -04:00
Andrew Clark
b8ed6a1aa5 [Scheduler] Call postTask directly (#19551)
This updates the experimental Scheduler postTask build to call postTask
directly, instead of managing our own custom queue and work loop.

We still use a deadline 5ms mechanism to implement `shouldYield`.

The main thing that postTask is currently missing is the continuation
feature — when yielding to the main thread, the yielding task is sent
to the back of the queue, instead of maintaining its position.

While this would be nice to have, even without it, postTask may be good
enough to replace our userspace implementation.

We'll run some tests to see.
2020-08-12 08:39:47 -05:00
Ricky
c8d9b8878a Speed up yarn cache in circle (#19566) 2020-08-11 14:05:15 -04:00
Dan Abramov
ce37bfad5f Remove resolutions from test renderer package.json (#19577) 2020-08-10 20:45:12 +01:00
Kartik Choudhary
2704bb5374 Add ReactVersion to SchedulingProfiler render scheduled marks (#19553)
* Add ReactVersion to SchedulingProfiler render scheduled marks

* Move ReactVersion to a new --react-init-* mark

Co-authored-by: E-Liang Tan <eliang@eliangtan.com>
2020-08-10 10:51:25 -04:00
Brian Vaughn
0c52e24cb6 Support inner component _debugOwner in memo (#19556)
* Support inner component _debugOwner in memo

* test with devtool context

* remove memo test

* Merged master; tweaked test and snapshot

* Pass owner to createFiber fn when creating a memo component.

Co-authored-by: Theodore Han <tqhan317@gmail.com>
2020-08-10 10:49:10 -04:00
Dan Abramov
94c0244bab Fix double-firing mouseenter (#19571)
* test: Simulate mouseover in browser

* Fix duplicate onMouseEnter event when relatedTarget is a root

* Test leave as well

Co-authored-by: Sebastian Silbermann <silbermann.sebastian@gmail.com>
2020-08-10 15:08:22 +01:00
Dan Abramov
aa99b0b08e Rename test files 2020-08-10 12:58:42 +01:00
Sebastian Silbermann
7f696bd99c fix: onFocus/onBlur/onBeforeInput have a matching event type (#19561)
* test: Add current behavior for event types of onFocus/onBlur

* fix: onFocus/onBlur have a matching event type

* fix useFocus

* fix: don't compare native event types with react event types

* Add FocusIn/FocusOutEventInterface

* A simpler alternative fix

* Add regression tests

* Always pass React event type and fix beforeinput

Co-authored-by: Dan Abramov <dan.abramov@me.com>
2020-08-10 12:54:10 +01:00
Sebastian Silbermann
7c30fb3b0b Update outdated lockfile (#19568) 2020-08-10 11:59:37 +01:00
Andrew Clark
0cd9a6de55 Parallelize Jest in CI (#19552)
Uses CircleCI's `parallelism` key to split our test jobs across multiple
processes, like we do for the build job.
2020-08-07 16:32:59 -04:00
Dan Abramov
2d9ec9199c Indent a command 2020-08-07 19:24:49 +01:00
Dan Abramov
db2f229110 Fix command 2020-08-07 19:23:43 +01:00
Dan Abramov
336729856a Nesting Fixture (#19531)
* Nesting Fixture

* Add README to nesting fixture

* Apply suggestions from code review

Co-authored-by: Ricky <rickhanlonii@gmail.com>

* Fixes

* Add Redux

* Use different versions

* Use Consumer API

* Rename helper

* Write docs

* Update README.md

* Update README.md

Co-authored-by: Rick Hanlon <rickhanlonii@gmail.com>
2020-08-07 02:08:41 +01:00
Brian Vaughn
a63893ff32 Warn about undefined return value for memo and forwardRef (#19550) 2020-08-06 16:12:32 -04:00
Ricky
32ff428687 Add feature flag for setting update lane priority (#19401)
* Add feature flag for setting update lane priority

* Remove second feature flag

* Refactor feature flag locations

* Add missing else
2020-08-06 12:47:32 -04:00
Gustavo Saiani
5bdd4c8c60 Remove unused argument from call to jest method (#19546) 2020-08-06 16:55:01 +01:00
Mateusz Burzyński
a5fed98a96 Register more node types that are used later as JSXIdentifiers (#19514) 2020-08-06 16:37:32 +01:00
DaniAcu
50893dbcb7 fix(react-dom): unnecesary path on DOMEventProperties (#19544) 2020-08-06 16:37:06 +01:00
Dominic Gannaway
f77c7b9d76 Re-add discrete flushing timeStamp heuristic (behind flag) (#19540) 2020-08-06 13:21:05 +01:00
Brian Vaughn
e67a6b1603 Fix runtime error that happens if a passive destroy function throws within an unmounted tree (#19543)
A passive effect's cleanup function may throw after an unmount. In that event, React sometimes threw an uncaught runtime error trying to access a property on a null stateNode field. This commit fixes that (and adds a regression test).
2020-08-05 18:22:18 -04:00
Emilis Baliukonis
5cff775502 [Scheduler] Get current time from performance.now in non-DOM environments (#19532)
* Get current time from performance.now in non-DOM environments

* Use local references to native APIs for Date and Performance

* Refactored to read globals directly
2020-08-05 17:29:06 +01:00
Dan Abramov
e9721e14e4 Remove onScroll bubbling flag (#19535) 2020-08-05 16:07:58 +01:00
Dominic Gannaway
b61174fb7b Remove the deprecated React Flare event system (#19520) 2020-08-05 15:13:29 +01:00
Halit Ogunc
8d57ca519a fix: typo in React Release Scripts (#19524) 2020-08-04 13:22:50 -04:00
Ricky
a437f3ff30 Use RN fork in default branch of feature flags (#19522) 2020-08-03 17:40:58 -04:00
Ricky
86314d5b45 Turn off new component stacks for React Native (#19521) 2020-08-03 14:45:50 -04:00
Andrew Clark
5f1890f12b Bugfix: Don't unmount siblings of deleted node (#19516)
* Test: Don't unmount siblings of deleted node

Adds a failing regression test. Will fix in the next commit.

* Refactor to accept deleted fiber, not child list

A deleted fiber is passed to
flushPassiveUnmountEffectsInsideOfDeletedTree, but the code is written
as if it accepts the first node of a child list. This is likely because
the function was based on similar functions like
`flushPassiveUnmountEffects`, which do accept a child list.

Unfortunately, types don't help here because we use the first node
in the list to represent the whole list, so in both cases, the type
is Fiber.

Might be worth changing the other functions to also accept individual
fibers instead of a child list, to help avoid confusion.

* Add layout effect to regression test, just in case
2020-08-03 08:57:41 -07:00
Andrew Clark
93a0c28305 Add static version of Passive subtree tag (#19510)
Creates new subtree tag, PassiveStatic, that represents whether a
tree contains any passive effect hooks.

It corresponds to the PassiveStatic effect tag, which represents the
same concept for an individual fiber.

This allows us to remove the PassiveStatic effect tag from PassiveMask.
Its presence was causing us to schedule a passive effect phase callback
on every render, instead of only when something changed. That's now
fixed; this is reflected in the SchedulerProfiler tests.

(The naming is getting really confusing. Need to do some bikeshedding.)
2020-08-01 08:15:46 -05:00
Dan Abramov
a1c0864d19 Support untagged releases (#19509) 2020-07-31 20:47:28 +01:00
Dan Abramov
5d271fc3b1 Revert "Support untagged releases (#19507)" (#19508)
This reverts commit 58b3ee7a88.
2020-07-31 19:57:32 +01:00
Dan Abramov
58b3ee7a88 Support untagged releases (#19507)
* Support untagged releases

* Fix
2020-07-31 19:18:37 +01:00
Dan Abramov
7543459a48 Allow publishing untagged releases (#19505) 2020-07-31 16:10:01 +01:00
Dan Abramov
3d0895557a Disable onScroll bubbling statically except for WWW (#19503) 2020-07-31 15:09:24 +01:00
Dan Abramov
332eceface Revert "Statically enable enableFilterEmptyStringAttributesDOM (#19502)" (#19504)
This reverts commit 815ee89bf5.
2020-07-31 15:01:27 +01:00
Dan Abramov
815ee89bf5 Statically enable enableFilterEmptyStringAttributesDOM (#19502) 2020-07-31 14:57:57 +01:00
Andrew Clark
ede9170648 Move passive logic out of layout phase (#19500)
* setCurrentFiber per fiber, instead of per effect

* Re-use safelyCallDestroy

Part of the code in flushPassiveUnmountEffects is a duplicate of the
code used for unmounting layout effects. I did some minor refactoring to
so we could use the same function in both places.

Closure will inline anyway so it doesn't affect code size or
performance, just maintainability.

* Don't check HookHasEffect during deletion

We don't need to check HookHasEffect during a deletion; all effects are
unmounted.

So we also don't have to set HookHasEffect during a deletion, either.

This allows us to remove the last remaining passive effect logic from
the synchronous layout phase.
2020-07-30 21:43:03 -07:00
Dan Abramov
22d16cc15d Remove event constants (#19276)
* Remove opaque event type

* Rename type and merge files

* Use literals where we have Flow coverage

* Flowify some plugins

* Remove constants except necessary ones
2020-07-30 21:52:37 +01:00
Andrew Clark
6ef997b534 Check for passive effects on the root fiber (#19488)
The root fiber doesn't have a parent from which we can read the
`subtreeTag`, so we need to check its `effectTag` directly.

The root fiber previously did not have any pending passive effects,
but it does now that deleted fibers are cleaned up in the passive phase.

This allows us to remove a `schedulePassiveEffectCallback` call from the
synchronous unmount path.

Co-authored-by: Brian Vaughn <bvaughn@fb.com>
2020-07-30 09:17:22 -04:00
Andrew Clark
e1f96b82b4 Check PassiveStatic instead of Passive (#19489)
Saves us from having to set a flag on `current` during the layout phase.

Could result in some redundant traversal, since PassiveStatic includes
effects that don't need clean-up. But it's worth it to remove the work
from the layout phase.

While I was editing this, I also re-arranged it so that we check the
`effectTag` check before we check the `tag`, since the `effectTag` check
is the one that's more likely to fail.
2020-07-30 09:13:04 -04:00
Dan Abramov
dff97a6915 Fix onGot/LostPointerCapture events (#19487) 2020-07-29 21:54:48 +01:00
Brian Vaughn
eae90cdbe9 Effects list refactor continued: passive effects traversal (#19374)
* Adds new `Passive` subtree tag value.
* Adds recursive traversal for passive effects (mounts and unmounts).
* Removes `pendingPassiveHookEffectsMount` and `pendingPassiveHookEffectsUnmount` arrays from work loop.
* Re-adds sibling and child pointer detaching (temporarily removed in previous PR).
* Addresses some minor TODO comments left over from previous PRs.

---

Co-authored-by: Luna Ruan <luna@fb.com>
2020-07-29 16:54:22 -04:00
Ricky
74cd7e5f17 Use feature flags for React Native in the test renderer (#19486) 2020-07-29 16:31:05 -04:00
Dan Abramov
291db05a75 Add regression tests for all events (#19485) 2020-07-29 21:13:56 +01:00
Ricky
7c8cc4358e Add postTask browser scheduler implementation (#19479)
* Reduce code to necessities

* Switch to postTask API

* Add SchedulerPostTask tests

* Updates from review

* Fix typo from review

* Generate build of unstable_post_task
2020-07-29 15:27:59 -04:00
Dan Abramov
0eea16601c Event propagation test suite (#19483) 2020-07-29 15:34:57 +01:00
Brian Vaughn
5227a37868 Add "unstable_" prefix to experimental mutable source APIs (#19472)
* Add "unstbale_" prefix to mutable source APIs

* DebugHooks no longer calls useMutableSource() on init

This was causing an observable behavioral difference between experimental DEV and PROD builds.

We don't initialize stack position for other composite hooks (e.g. useDeferredValue, useTransition, useOpaqueIdentifier). If we did, it would cause the same obesrvable behavioral difference.
2020-07-28 12:38:13 -04:00
Ricky
52c5146274 Add SchedulerHostConfig fork for post task (#19470) 2020-07-28 11:11:31 -04:00
Andrew Clark
722bc046dc Don't rely on didTimeout for SyncBatched (#19469)
Tasks with SyncBatchedPriority — used by Blocking Mode — should always
be rendered by the `peformSyncWorkOnRoot` path, not
`performConcurrentWorkOnRoot`.

Currently, they go through the `performConcurrentWorkOnRoot` callback.
Then, we check `didTimeout` to see if the task expired. Since
SyncBatchedPriority translates to ImmediatePriority in the Scheduler,
`didTimeout` is always `true`, so we mark it as expired. Then it exits
and re-enters in the `performSyncWorkOnRoot` path.

Aside from being overly convoluted, we shouldn't rely on Scheduler to
tell us that SyncBatchedPriority work is synchronous. We should handle
that ourselves.

This will allow us to remove the `didTimeout` check. And it further
decouples us from the Scheduler priority, so we can eventually remove
that, too.
2020-07-27 14:42:44 -07:00
Sebastian Silbermann
feb134c30d feat(react-dom): Add enterKeyHint (#18634) 2020-07-27 21:07:08 +01:00
Andrew Clark
96ac799eac Remove callbackId field from FiberRoot (#19458)
The old expiration times implementation used this field to infer when
the priority of a task had changed at a more granular level than a
Scheduler priority level.

Now that we have the LanePriority type, which is React-specific, we no
longer need the `callbackId` field.
2020-07-27 10:39:07 -07:00
Andrew Clark
c24b641b76 Remove timeout option from scheduleCallback (#19457)
Since the Lanes refactor landed, we no longer rely on this anywhere, so
we can remove it.

The `delay` option is still needed by our timer implementation
(setTimeout polyfill). We'll keep the feature, but we'll likely change
how it's exposed once we figure out the proper layering between the
various Scheduler APIs.
2020-07-27 09:56:25 -07:00
Dominic Gannaway
144297165b Bail-out of attaching non-delegated listeners (#19466)
* Bail-out of attaching non-delegated listeners

Revise comment

* Fix tests/add tests

* Add onInvalid test
2020-07-27 17:47:14 +01:00
Dan Abramov
06d104e8ec Don't emulate bubbling of the scroll event (#19464)
* Don't emulate bubbling of the scroll event

* Put behind a flag
2020-07-27 17:33:54 +01:00
Dominic Gannaway
217ecf581b Re-added toggle event to non-delegated events (#19465) 2020-07-27 16:58:38 +01:00
Dominic Gannaway
6bb86fd162 Delegated all capture events (#19463) 2020-07-27 15:36:43 +01:00
Dominic Gannaway
05344faca4 Follow up fix to 19452 (#19454) 2020-07-27 15:03:11 +01:00
Ricky
d29bf59a6e Update my mailmap entries (#19455) 2020-07-26 22:47:32 -04:00
Ashwin Ramaswami
5530f1ccfb Upgrade art fixtures to babel 7 (#16235)
* chore: upgrade art fixtures to babel 7

* upgrade

* use yarn
2020-07-25 23:43:49 -04:00
Dominic Gannaway
242a50a652 Fix issue with capture phase non-bubbling events (#19452) 2020-07-25 12:32:21 +01:00
Dominic Gannaway
ef22aecfc5 Add invalid/close events to <dialog> element (#19439)
Support dialog

Fix
2020-07-24 20:36:46 +01:00
Robert Ferentz
b55f75d0a5 fix issue #19362 + test (#19412) 2020-07-24 14:34:16 -04:00
Sebastian Markbåge
d93c8faada Move Persistent Mode Optimization away from firstEffect (#19381)
Persistent mode needs to clone a parent and add its children if a child has
changed.

We have an optimization in persistent mode where we don't do that if no
child could've changed. If there are no effects scheduled for any child
then there couldn't have been changes.

Instead of checking for this on firstEffect, we now check this on the
children's effectTag and subtreeTags.

This is quite unfortunate because if we could just do this check a little
bit later we would've already gotten it transferred to the completed work's
subtreeTag. Now we have to loop over all the children and if any of them
changed, we have to loop over them again. Doing at least two loops per
parent.
2020-07-24 09:31:30 -07:00
Ricky
909b612e27 Sync React Native types (#19437) 2020-07-22 22:56:05 -04:00
Dan Abramov
76ce685d0f Remove SyntheticEvent subtypes (#19436)
* Remove SyntheticEvent subtypes

* Code golf
2020-07-23 01:02:59 +01:00
Dominic Gannaway
30e3cfe406 Fix scope issue (#19434) 2020-07-22 23:05:50 +01:00
Dan Abramov
125d1a19a2 Move event things around (#19433)
* Put all synthetic events in one file

* Inline other helpers

* Remove Modern from filenames
2020-07-22 22:12:15 +01:00
Dominic Gannaway
1aae05c436 Revise comment + add non-bubbling event test (#19432) 2020-07-22 21:41:40 +01:00
Dominic Gannaway
356c17108f Remove capturePhaseEvents and separate events by bubbling (#19278)
* Remove capturePhaseEvents and separate events by bubbling

WIP

Refine all logic

Revise types

Fix

Fix conflicts

Fix flags

Fix

Fix

Fix test

Revise

Cleanup

Refine

Deal with replaying

Fix

* Add non delegated listeners unconditionally

* Add media events

* Fix a previously ignored test

* Address feedback

Co-authored-by: Dan Abramov <dan.abramov@me.com>
2020-07-21 22:40:50 +01:00
Dan Abramov
1dcee86565 Regression test for media event bubbling (#19428) 2020-07-21 22:26:09 +01:00
Ricky
c9749d38e3 Generate RN renderers for stable builds (#19421)
* Generate RN renderers for experimental builds

* Don't generate FB builds for experimental channels
2020-07-20 17:15:06 -04:00
Sebastian Markbåge
ab1f517000 isFBBundle should be true if it's FB for www or FB for RN (#19420)
This ensures that the .fb.js override files kick in for RN. Otherwise we
won't have FB specific exports in the isomorphic modules.
2020-07-20 10:37:04 -07:00
Haseeb Khan
ecbdb74d7c Added the word "events" to clear ambiguousness. (#19409) 2020-07-20 18:26:41 +01:00
Andrew Clark
b23ea02be5 Track event times per lane on the root (#19387)
* Pass event time to markRootUpdated

Some minor rearranging so that eventTime gets threaded through. No
change in behavior.

* Track event times per lane on the root

Previous strategy was to store the event time on the update object
and accumulate the most recent one during the render phase.

Among other advantages, by tracking them on the root, we can read the
event time before the render phase has finished.

I haven't removed the `eventTime` field from the update object yet,
because it's still used to compute the timeout. Tracking the timeout
on the root is my next step.
2020-07-17 10:47:39 -07:00
Ricky
faa697f4f9 Set current update lane priority for user blocking events (#19342)
* Set current update lane priority for user blocking events

* Update to use LanePriority and not use runWithPriority

* Remove unused imports

* Fix tests, and I missed ReactDOMEventListener

* Fix more tests

* Add try/finally and hardcode lane priorities instead

* Also hard code InputContinuousLanePriority in tests

* Remove un-needed exports

* Comment rollbacks
2020-07-17 12:58:44 -04:00
Dominic Gannaway
4c8c98ab9b Sync latest useFocus/useFocusWithin changes (#19397) 2020-07-17 16:26:40 +01:00
Brian Vaughn
51267c4ac9 Sync scheduling profiler marks and debug tracing to new reconciler fork (#19375, #19376, #19396)
* Make enableSchedulingProfiler flag static

* Copied debug tracing and scheduler profiling to .new fork and updated feature flags

* Move profiler component stacks behind a feature flag
2020-07-17 11:24:26 -04:00
Dan Abramov
aec934af7f Remove form event delegation flag (#19395) 2020-07-17 14:19:39 +01:00
Dominic Gannaway
a59f899a1d Modern Event System: use focusin/focusout for onFocus/onBlur (#19186) 2020-07-16 18:12:03 +01:00
Dominic Gannaway
9102719baa Tidy up React Scope API (#19352) 2020-07-16 16:21:21 +01:00
Ricky
bc4cd92cd5 Use jest-environment-jsdom-sixteen (#19288)
* Use jest-environment-jsdom-sixteen

* Update yarn.lock

* Dedupe new jest packages in yarn.lock

* Pull upstream changes
2020-07-16 10:17:31 -04:00
Dominic Gannaway
e387c98ffa Fix bug with enableLegacyFBSupport click handlers (#19378) 2020-07-16 14:56:31 +01:00
Brian Vaughn
a226b9b445 Effects list refactor continued: did-bailout flag (#19322)
* Effects list rewrite

* Improved deletions approach

Process deletions as we traverse the tree during commit, before we process other effects. This has the result of better mimicking the previous sequencing.

* Made deletions field nullable

* Revert (no longer necessary) change to ReactNative test

* Eagerly set Deletions effect on Fiber when adding child to deletions array

* Initialize deletions array to null

* Null out deletions array instead of splicing 🤡

* Removed TODO comment

* Initial exploration on a did-bailout flag

* fixed the rest of the bugs

* Rolled temporary didBailout attribute into subtreeTag

* addressed comments

* Removed DidBailout subtree tag

* Removed stale comment

* use while loop instead of recursion for siblings

* move bailout flag from while loop

* Removed some unnecessary Deletion effectTags from children

* Move Deletion effect assignment to deletions array initialization

Co-authored-by: Luna <lunaris.ruan@gmail.com>
2020-07-16 09:10:00 -04:00
Brian Vaughn
fed4ae0247 DevTools 4.8.1 -> 4.8.2 (and CHANGELOG) 2020-07-15 12:37:08 -04:00
Brian Vaughn
a89854bc93 Fix Suspense-wrapping heuristic (and bump version numbers) (#19373)
* Fixed suspense wrapping heuristic

* Bump package numbers 16.13.1 -> 17.0.0-alpha.0 to fix DevTools Suspense heuristic
2020-07-15 12:25:27 -04:00
Dan Abramov
4961833dbf Don't build shallow renderer UMD bundles 2020-07-15 16:53:02 +01:00
Brian Vaughn
a9ba5283d9 Disable DevTools minification (#19369)
DevTools isn't being downloaded like typical JavaScript, so bundle size concerns don't apply. Parsing is still a consideration (so I'm open for discussion here) but I think this change would provide a couple of benefits:
* People are more likely to *actually read* non-minified source code when e.g. a breakpoint is hit (as with the recent debugger statement)
* Component stacks will be easier to parse on bug reports
2020-07-15 10:38:30 -04:00
Dominic Gannaway
45eef8b6b5 Devtools: improve getID guard (#19364) 2020-07-15 14:17:48 +01:00
Dominic Gannaway
392277f0ab Revert "Scheduling profiler updates (#19334)" (#19366)
This reverts commit 6d7555b014.
2020-07-15 12:36:40 +01:00
Dominic Gannaway
c604b69973 Revert "Revert "Fix createEventHandle bug with comment containers (#19348)" (#19354)" (#19365)
This reverts commit 566f613763.
2020-07-15 12:35:46 +01:00
Dominic Gannaway
566f613763 Revert "Fix createEventHandle bug with comment containers (#19348)" (#19354)
This reverts commit 147179ae82.
2020-07-15 12:27:22 +01:00
Dominic Gannaway
147179ae82 Fix createEventHandle bug with comment containers (#19348) 2020-07-14 13:09:39 +01:00
Brian Vaughn
6d7555b014 Scheduling profiler updates (#19334)
* Make enableSchedulingProfiler static for profiling+experimental builds

* Copied debug tracing and scheduler profiling to .new fork

* Updated test @gate conditions
2020-07-13 22:20:53 -04:00
Bogdan Ponomarenko
9ea0f6752d fix mistake in React devtools (#19336)
Co-authored-by: Пономаренко Богдан <b.ponomarenko@tinkoff.ru>
2020-07-13 16:49:52 -04:00
Brian Vaughn
fbc6386369 Fix DevTools handling of empty Suspense tag for legacy renderer versions (#19337) 2020-07-13 16:21:56 -04:00
Andrew Clark
d1f2143aa6 Use RetryLanes instead of event time (follow up) (#19335)
Same logic as #19307. Uses RetryLanes instead of event time to
determine if a render includes new updates.
2020-07-13 10:37:33 -07:00
Dan Abramov
fe19b4247b eslint-plugin-react-hooks@4.0.8 2020-07-13 17:58:49 +01:00
Bianca Del Carretto
84479046f7 reference not add to dependencies if only its type is used as typeof arg (#19316)
* reference not add to dependencies if only its type is used as typeof arg

* Add a few more tests
2020-07-13 17:57:00 +01:00
Dan Abramov
26472c8897 Bubble onSubmit/onReset behind a feature flag (#19333) 2020-07-13 17:17:28 +01:00
Sebastian Silbermann
e2d73e91cc chore: Build react-dom/test-utils in codesandbox deploy (#19321) 2020-07-13 16:15:37 +01:00
Dominic Gannaway
61dd00db24 Extract some of the tidy up changes from 19278 (#19315) 2020-07-10 22:33:42 +01:00
Dan Abramov
a2b4db05bc eslint-plugin-react-hooks@4.0.7 2020-07-10 19:08:05 +01:00
Dan Abramov
47915fd6e1 [ESLint] Fix a bug causing a too coarse dependency suggestion (#19313)
* Add regression test for ESLint rule

* Fix the issue
2020-07-10 19:02:08 +01:00
Brian Vaughn
d5d659062d Updated DevTools test snapshots 2020-07-10 13:06:50 -04:00
Dominic Gannaway
2562e758ea Revise useFocus/useFocusWithin (#19310) 2020-07-10 17:23:00 +01:00
Nick Reiley
17efbf7d63 [DevTools] Show HOC names in profiler (#19283)
* show hoc names in profiler

* Added hocDisplayNames.length check

Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2020-07-10 11:21:19 -04:00
Andrew Clark
970fa122d8 Use lanes to check if a render is a Suspense retry (#19307)
Now that Suspense retries have their own dedicated set of lanes
(#19287), we can determine if a render includes only retries by checking
if its lanes are a subset of the retry lanes.

Previously we inferred this by checking
`workInProgressRootLatestProcessedEventTime`. If it's not set, that
implies that no updates were processed in the current render, which
implies it must be a Suspense retry. The eventual plan is to get rid of
`workInProgressRootLatestProcessedEventTime` and instead track event
times on the root; this change is one the steps toward that goal.

The relevant tests were originally added in #15769.
2020-07-10 08:58:05 -05:00
Dan Abramov
2663a12eb3 Tweak wording 2020-07-10 14:56:28 +01:00
Dan Abramov
d4eadea6c7 React DevTools 4.8.1 2020-07-10 14:11:55 +01:00
Dan Abramov
8eaf05e0e8 DevTools: Make break-on-warn off by default (#19309) 2020-07-10 14:11:32 +01:00
Andrew Clark
14084be286 Refactor algorithm for next Lanes to work on (#19302)
Some clean up to make the Lanes type easier to maintain.

I removed the "start" and "end" range markers; they don't provide any
information that isn't already encoded in the bitmask for each range,
and there's no computation saved compared to the
`pickArbitraryLane` function.

The overall algorithm is largely the same but I did tweak some of the
details. For example, if the lanes for a given priority are already
being worked on, the previous algorithm would assign to the next
available lane, including the dedicated hydration lanes that exist
in between each priority.

The updated algorithm skips over the hydration lanes and goes to the
next priority level. In the rare instance when all the non-Idle update
lanes are occupied, it will pick an abitrary default lane. This will
have the effect of invalidating the current work-in-progress, and
indicates a starvation scenario.

Eventually, if there are too many interruptions, the expiration time
mechanism will kick in and force the update to synchronously finish.
2020-07-09 18:53:54 -05:00
Brian Vaughn
08b8e6e1dc DevTools 4.7.0 -> 4.8.0 (#19297) 2020-07-09 10:49:44 -04:00
Brian Vaughn
e760d1fb0f Fixed test script for DevTools build config (#19295) 2020-07-09 10:49:33 -04:00
Gianmarco
a5b4492950 Updates Electron version for react-devtools (#19280) 2020-07-09 10:13:26 -04:00
Rimil Dey
6508ab3be8 Fix typo in Overview doc for react-devtools (#19289) 2020-07-09 09:13:15 -04:00
Andrew Clark
965d08cfff Add dedicated LanePriority for Suspense retries (#19287)
A "retry" is a special type of update that attempts to flip a Suspense
boundary from its placeholder state back to its primary/resolved state.

Currently, retries are given default priority, using the same algorithm
as normal updates and occupying range of lanes.

This adds a new range of lanes dedicated specifically to retries, and
gives them lower priority than normal updates.

A couple of existing tests were affected because retries are no longer
batched with normal updates; they commit in separate batches.

Not totally satisfied with this design, but I think things will snap more
into place once the rest of the Lanes changes (like how we handle
parallel Suspense transitions) have settled.
2020-07-08 21:09:22 -07:00
Dominic Gannaway
766af5968a Move Scope API ref resolution to mutation phase (#19264)
* Move Scope API ref resolution to mutation phase
2020-07-08 23:41:39 +01:00
Dan Abramov
c7805b8d57 Use a custom jsdom fork (#19284) 2020-07-08 21:26:54 +01:00
Dan Abramov
e0e6b9c036 Add regression test for #19269 (#19282) 2020-07-08 20:48:43 +01:00
Behnam Mohammadi
77e872217c Improve readability of isValidElementType (#19251)
* improve readability

* replace condition by switch/case

* replace condition by switch/case

* remove unnecessary braces

* replace switch/case to ifs

* replace switch/case to ifs

* fix by multiline if statements

* fix multiple if statements
2020-07-08 19:25:24 +01:00
Dan Abramov
7b0ef42bcc Update README.md 2020-07-08 18:30:18 +01:00
Giovanni Fumagalli
d87220f217 Add console group methods to dev disabled groups (#19069)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2020-07-08 17:28:49 +01:00
Dominic Gannaway
f5ea39c182 Modern Event System: make on*Capture events use capture phase (#19221) 2020-07-08 17:10:06 +01:00
falguniraina
e18fb91852 Text improvement (#19167)
text improvement in README.md
2020-07-08 17:07:27 +01:00
E-Liang Tan
40cddfeeb1 Add user timing marks for scheduling profiler tool (#19223)
High level breakdown of this commit:

* Add a enableSchedulingProfiling feature flag.
* Add functions that call User Timing APIs to a new SchedulingProfiler file. The file follows DebugTracing's structure.
* Add user timing marks to places where DebugTracing logs.
* Add user timing marks to most other places where @bvaughn's original draft DebugTracing branch marks.
* Tests added
* More context (and discussions with @bvaughn) available at our internal PR MLH-Fellowship#11 and issue MLH-Fellowship#5.

Similar to DebugTracing, we've only added scheduling profiling calls to the old reconciler fork.

Co-authored-by: Kartik Choudhary <kartik.c918@gmail.com>
Co-authored-by: Kartik Choudhary <kartikc.918@gmail.com>
Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2020-07-08 10:36:02 -04:00
Phil MacCart
b85b47630b Fix state leaking when a function component throws on server render (#19212)
* add unit test asserting internal hooks state is reset

* Reset internal hooks state before rendering

* reset hooks state on error

* Use expect...toThrow instead of try/catch in test

* reset dev-only hooks state inside resetHooksState

* reset currentlyRenderingComponent to null
2020-07-08 03:10:23 +01:00
Dan Abramov
6fd4321135 eslint-plugin-react-hooks@4.0.6 2020-07-07 22:39:40 +01:00
Dan Abramov
7c35cb20ef [ESLint] Handle optional member chains (#19275)
* Rename internal variables

This disambiguates "optional"/"required" because that terminology is taken by optional chaining.

* Handle optional member chains

* Update comment

Co-authored-by: Ricky <rickhanlonii@gmail.com>

Co-authored-by: Ricky <rickhanlonii@gmail.com>
2020-07-07 21:34:16 +01:00
Ricky
dccf541ec5 Allow decoupleUpdatePriorityFromScheduler to be set (#19272) 2020-07-07 12:58:10 -04:00
Dan Abramov
7ca1d861e8 [ESLint] Consistently treat optional chaining as regular chaining (#19273)
* Revert "Fix ExhaustiveDeps ESLint rule throwing with optional chaining (#19260)"

This reverts commit 0f84b0f02b.

* Re-add a test from #19260

* Remove all code for optional chaining support

* Consistently treat optional chaining as regular chaining

This is not ideal because our suggestions use normal chaining. But it gets rid of all current edge cases.

* Add more tests

* More consistency in treating normal and optional expressions

* Add regression tests for every occurrence of Optional*
2020-07-07 17:38:44 +01:00
Dominic Gannaway
98390f11f6 Update useEvent implementation (#19271) 2020-07-07 14:44:56 +01:00
Dominic Gannaway
4eb9b1d2b4 Refactor createEventHandle signature (#19174) 2020-07-07 13:05:06 +01:00
Dominic Gannaway
97b96da295 Fix event replaying logic (#19269) 2020-07-07 02:28:55 +01:00
Ricky
91a2e8173f Decouple update priority tracking from Scheduler package (#19121)
* Initial currentLanePriority implementation

* Minor updates from review

* Fix typos and enable flag

* Fix feature flags and lint

* Fix simple event tests by switching to withSuspenseConfig

* Don't lower the priority of setPending in startTransition below InputContinuous

* Move currentUpdateLanePriority in commit root into the first effect block

* Refactor requestUpdateLane to log for priority mismatches

Also verifies that the update lane priority matches the scheduler lane priority before using it

* Fix four tests by adding ReactDOM.unstable_runWithPriority

* Fix partial hydration when using update lane priority

* Fix partial hydration when using update lane priority

* Rename feature flag and only log for now

* Move unstable_runWithPriority to ReactFiberReconciler

* Add unstable_runWithPriority to ReactNoopPersistent too

* Bug fixes and performance improvements

* Initial currentLanePriority implementation

* Minor updates from review

* Fix typos and enable flag

* Remove higherLanePriority from ReactDOMEventReplaying.js

* Change warning implementation and startTransition update lane priority

* Inject reconciler functions to avoid importing src/

* Fix feature flags and lint

* Fix simple event tests by switching to withSuspenseConfig

* Don't lower the priority of setPending in startTransition below InputContinuous

* Move currentUpdateLanePriority in commit root into the first effect block

* Refactor requestUpdateLane to log for priority mismatches

Also verifies that the update lane priority matches the scheduler lane priority before using it

* Fix four tests by adding ReactDOM.unstable_runWithPriority

* Fix partial hydration when using update lane priority

* Fix partial hydration when using update lane priority

* Rename feature flag and only log for now

* Move unstable_runWithPriority to ReactFiberReconciler

* Bug fixes and performance improvements

* Remove higherLanePriority from ReactDOMEventReplaying.js

* Change warning implementation and startTransition update lane priority

* Inject reconciler functions to avoid importing src/

* Fixes from bad rebase
2020-07-06 18:53:42 -04:00
Dominic Gannaway
c3e42a962b Fix master tests (#19267) 2020-07-06 17:36:19 -04:00
Dominic Gannaway
0c0aaeb6bc Handle test-cli failure case for CI (#19265) 2020-07-06 22:05:51 +01:00
Joe Lencioni
0f84b0f02b Fix ExhaustiveDeps ESLint rule throwing with optional chaining (#19260)
Certain code patterns using optional chaining syntax causes
eslint-plugin-react-hooks to throw an error.

We can avoid the throw by adding some guards. I didn't read through the
code to understand how it works, I just added a guard to every place
where it threw, so maybe there is a better fix closer to the root cause
than what I have here.

In my test case, I noticed that the optional chaining that was used in
the code was not included in the suggestions description or output,
but it seems like it should be. This might make a nice future
improvement on top of this fix, so I left a TODO comment to that effect.

Fixes #19243
2020-07-06 15:52:14 -04:00
Brian Vaughn
670c0376ea Enable createEventHandle API for wWW test builds (#19262) 2020-07-06 15:19:50 -04:00
Dominic Gannaway
26071abbe1 Refine event registration + event signatures (#19244)
* Refine event registration + event signatures

* Address feedback
2020-07-06 17:42:01 +01:00
Ittai Baratz
1cbaf48889 Add SSL support to React devtools standalone (#19191)
Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2020-07-06 10:09:41 -04:00
Dominic Gannaway
65c137768a Fix Flow from #19239 (#19242) 2020-07-02 16:37:10 +01:00
Son Ki-Kyun
cd9046f815 Add some type detection (#19239) 2020-07-02 15:54:36 +01:00
Dan Abramov
b6df4417c7 Remove event config (#19237) 2020-07-02 00:26:29 +01:00
Dan Abramov
991c3b8193 Make event config an implementation detail of each plugin (#19236)
* Merge two variables with same purpose

* Replace dispatchConfig with _reactName on event object
2020-07-02 00:03:17 +01:00
Dan Abramov
b683c07ccc Remove TestUtils dependency on event registry (#19235) 2020-07-01 22:54:28 +01:00
550 changed files with 43369 additions and 28936 deletions

View File

@@ -8,16 +8,17 @@ aliases:
TZ: /usr/share/zoneinfo/America/Los_Angeles
- &restore_yarn_cache
restore_cache:
name: Restore yarn cache
key: v2-node-{{ arch }}-{{ checksum "yarn.lock" }}-yarn
- &restore_node_modules
restore_cache:
name: Restore node_modules cache
keys:
- v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
- v2-node-{{ arch }}-{{ .Branch }}-
- v2-node-{{ arch }}-
- &run_yarn
run:
name: Install Packages
command: yarn --frozen-lockfile
- v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-modules
- &TEST_PARALLELISM 20
- &attach_workspace
at: build
@@ -28,8 +29,7 @@ aliases:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: node ./scripts/rollup/consolidateBundleSizes.js
- run: ./scripts/circleci/pack_and_store_artifact.sh
- store_artifacts:
@@ -58,12 +58,26 @@ jobs:
name: Nodejs Version
command: node --version
- *restore_yarn_cache
- *run_yarn
- run:
name: Install Packages
command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save node_modules cache
key: v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
# Store the yarn cache globally for all lock files with this same
# checksum. This will speed up the setup job for all PRs where the
# lockfile is the same.
name: Save yarn cache for future installs
key: v2-node-{{ arch }}-{{ checksum "yarn.lock" }}-yarn
paths:
- ~/.cache/yarn
- save_cache:
# Store node_modules for all jobs in this workflow so that they don't
# need to each run a yarn install for each job. This will speed up
# all jobs run on this branch with the same lockfile.
name: Save node_modules cache
# This cache key is per branch, a yarn install in setup is required.
key: v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-modules
paths:
- node_modules
yarn_lint:
docker: *docker
@@ -71,8 +85,7 @@ jobs:
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: node ./scripts/prettier/index
- run: node ./scripts/tasks/eslint
- run: ./scripts/circleci/check_license.sh
@@ -85,138 +98,136 @@ jobs:
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: node ./scripts/tasks/flow-ci
RELEASE_CHANNEL_stable_yarn_test:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=stable --ci
yarn_test:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --ci
RELEASE_CHANNEL_stable_yarn_test_www:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-classic --ci
RELEASE_CHANNEL_stable_yarn_test_www_variant:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-classic --variant --ci
RELEASE_CHANNEL_stable_yarn_test_prod_www:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-classic --prod --ci
RELEASE_CHANNEL_stable_yarn_test_prod_www_variant:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-classic --prod --variant --ci
yarn_test_www:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-modern --ci
yarn_test_www_variant:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-modern --variant --ci
yarn_test_prod_www:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-modern --prod --ci
yarn_test_prod_www_variant:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=www-modern --prod --variant --ci
RELEASE_CHANNEL_stable_yarn_test_persistent:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=stable --persistent --ci
RELEASE_CHANNEL_stable_yarn_test_prod:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=stable --prod --ci
yarn_test_prod:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=experimental --prod --ci
RELEASE_CHANNEL_stable_yarn_build:
docker: *docker
environment: *environment
parallelism: 20
parallelism: *TEST_PARALLELISM
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run:
environment:
RELEASE_CHANNEL: stable
@@ -242,8 +253,7 @@ jobs:
parallelism: 20
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run:
environment:
RELEASE_CHANNEL: experimental
@@ -266,12 +276,14 @@ jobs:
build_devtools_and_process_artifacts:
docker: *docker
environment: *environment
parallelism: 20
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run:
name: Install Packages
command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
- run:
environment:
RELEASE_CHANNEL: experimental
@@ -279,6 +291,46 @@ jobs:
- store_artifacts:
path: ./build/devtools.tgz
build_devtools_scheduling_profiler:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *restore_node_modules
- run:
name: Install Packages
command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
- run:
name: Build and archive
command: |
mkdir -p build/devtools
cd packages/react-devtools-scheduling-profiler
yarn build
cd dist
tar -zcvf ../../../build/devtools-scheduling-profiler.tgz .
- store_artifacts:
path: ./build/devtools-scheduling-profiler.tgz
- persist_to_workspace:
root: packages/react-devtools-scheduling-profiler
paths:
- dist
deploy_devtools_scheduling_profiler:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: packages/react-devtools-scheduling-profiler
- *restore_node_modules
- run:
name: Deploy
command: |
cd packages/react-devtools-scheduling-profiler
yarn vercel deploy dist --prod --confirm --token $SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN
# These jobs are named differently so we can distinguish the stable and
# and experimental artifacts
process_artifacts: *process_artifacts
@@ -290,8 +342,7 @@ jobs:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
# This runs in the process_artifacts job, too, but it's faster to run
# this step in both jobs instead of running the jobs sequentially
- run: node ./scripts/rollup/consolidateBundleSizes.js
@@ -306,8 +357,7 @@ jobs:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
# This runs in the process_artifacts job, too, but it's faster to run
# this step in both jobs instead of running the jobs sequentially
- run: node ./scripts/rollup/consolidateBundleSizes.js
@@ -322,8 +372,7 @@ jobs:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn lint-build
- run: scripts/circleci/check_minified_errors.sh
@@ -333,8 +382,7 @@ jobs:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run:
environment:
RELEASE_CHANNEL: stable
@@ -344,21 +392,21 @@ jobs:
RELEASE_CHANNEL_stable_yarn_test_build:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=stable --build --ci
yarn_test_build:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=experimental --build --ci
yarn_test_build_devtools:
@@ -367,8 +415,7 @@ jobs:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --project=devtools --build --ci
RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
@@ -377,7 +424,7 @@ jobs:
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *restore_node_modules
- run:
name: Run DOM fixture tests
environment:
@@ -393,8 +440,7 @@ jobs:
environment: *environment
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run:
name: Run fuzz tests
command: |
@@ -404,21 +450,21 @@ jobs:
RELEASE_CHANNEL_stable_yarn_test_build_prod:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=stable --build --prod --ci
yarn_test_build_prod:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
steps:
- checkout
- attach_workspace: *attach_workspace
- *restore_yarn_cache
- *run_yarn
- *restore_node_modules
- run: yarn test --release-channel=experimental --build --prod --ci
workflows:
@@ -517,10 +563,19 @@ workflows:
- yarn_test_build_devtools:
requires:
- yarn_build
# FIXME: Temporarily disabled to unblock master.
# - build_devtools_and_process_artifacts:
# requires:
# - yarn_build
- build_devtools_and_process_artifacts:
requires:
- yarn_build
- build_devtools_scheduling_profiler:
requires:
- yarn_build
- deploy_devtools_scheduling_profiler:
requires:
- build_devtools_scheduling_profiler
filters:
branches:
only:
- master
fuzz_tests:
triggers:

View File

@@ -1,6 +1,6 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/scheduler"],
"buildCommand": "build --type=NODE react/index,react-dom/index,react-dom/server,scheduler/index,scheduler/tracing",
"buildCommand": "build --type=NODE react/index,react-dom/index,react-dom/server,react-dom/test-utils,scheduler/index,scheduler/tracing",
"publishDirectory": {
"react": "build/node_modules/react",
"react-dom": "build/node_modules/react-dom",

View File

@@ -18,4 +18,6 @@ packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/shared/build
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-shell/dist
packages/react-devtools-scheduling-profiler/dist
packages/react-devtools-scheduling-profiler/static

View File

@@ -16,7 +16,13 @@ module.exports = {
// Stop ESLint from looking for a configuration file in parent folders
root: true,
plugins: ['jest', 'no-for-of-loops', 'react', 'react-internal'],
plugins: [
'jest',
'no-for-of-loops',
'no-function-declare-after-return',
'react',
'react-internal',
],
parser: 'babel-eslint',
parserOptions: {
@@ -91,6 +97,9 @@ module.exports = {
// You can disable this rule for code that isn't shipped (e.g. build scripts and tests).
'no-for-of-loops/no-for-of-loops': ERROR,
// Prevent function declarations after return statements
'no-function-declare-after-return/no-function-declare-after-return': ERROR,
// CUSTOM RULES
// the second argument of warning/invariant should be a literal string
'react-internal/no-primitive-constructors': ERROR,
@@ -99,6 +108,18 @@ module.exports = {
'react-internal/warning-args': ERROR,
'react-internal/no-production-logging': ERROR,
'react-internal/no-cross-fork-imports': ERROR,
'react-internal/no-cross-fork-types': [
ERROR,
{
old: [
'firstEffect',
'nextEffect',
// Disabled because it's also used by the Hook type.
// 'lastEffect',
],
new: ['subtreeFlags'],
},
],
},
overrides: [
@@ -166,6 +187,12 @@ module.exports = {
__webpack_require__: true,
},
},
{
files: ['packages/scheduler/**/*.js'],
globals: {
TaskController: true,
},
},
],
globals: {

3
.gitignore vendored
View File

@@ -34,4 +34,5 @@ packages/react-devtools-extensions/firefox/*.pem
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/.tempUserDataDir
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-shell/dist
packages/react-devtools-scheduling-profiler/dist

View File

@@ -127,6 +127,8 @@ Rainer Oviir <roviir@gmail.com> <raineroviir@rainers-MacBook-Pro.local>
Ray <ray@tomo.im>
Richard Feldman <richard.t.feldman@gmail.com> <richard@noredink.com>
Richard Livesey <Livesey7@hotmail.co.uk>
Rick Hanlon <rickhanlonii@gmail.com>
Rick Hanlon <rickhanlonii@gmail.com> <rickhanlonii@fb.com>
Rob Arnold <robarnold@cs.cmu.edu>
Robert Binna <rbinna@gmail.com> <speedskater@users.noreply.github.com>
Robin Frischmann <robin@rofrischmann.de>
@@ -145,6 +147,7 @@ Steven Luscher <react@steveluscher.com> <github@steveluscher.com>
Steven Luscher <react@steveluscher.com> <steveluscher@fb.com>
Steven Luscher <react@steveluscher.com> <steveluscher@instagram.com>
Steven Luscher <react@steveluscher.com> <steveluscher@users.noreply.github.com>
Seth Webster <sethwebster@gmail.com> <sethwebster@fb.com>
Stoyan Stefanov <ssttoo@ymail.com>
Tengfei Guo <terryr3rd@yeah.net> <tfguo369@gmail.com>
Thomas Aylott <oblivious@subtlegradient.com> <aylott@fb.com>

View File

@@ -3,4 +3,6 @@ packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/shared/build
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-shell/dist
packages/react-devtools-scheduling-profiler/dist
packages/react-devtools-scheduling-profiler/static

View File

@@ -56,7 +56,7 @@ You'll notice that we used an HTML-like syntax; [we call it JSX](https://reactjs
## Contributing
The main purpose of this repository is to continue to evolve React core, making it faster and easier to use. Development of React happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving React.
The main purpose of this repository is to continue evolving React core, making it faster and easier to use. Development of React happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving React.
### [Code of Conduct](https://code.fb.com/codeofconduct)

View File

@@ -1,10 +1,10 @@
{
"devDependencies": {
"babel-core": "^6.24.0",
"babel-loader": "^6.4.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"@babel/core": "^7.10.5",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"react": "link:../../build/node_modules/react",
"react-art": "link:../../build/node_modules/react-art/",
"react-dom": "link:../../build/node_modules/react-dom",

View File

@@ -1,5 +1,4 @@
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: __dirname,
@@ -12,10 +11,10 @@ module.exports = {
exclude: /node_modules/,
query: {
presets: [
require.resolve('babel-preset-es2015'),
require.resolve('babel-preset-react'),
require.resolve('@babel/preset-env'),
require.resolve('@babel/preset-react'),
],
plugins: [require.resolve('babel-plugin-transform-class-properties')],
plugins: [require.resolve('@babel/plugin-proposal-class-properties')],
},
},
],

File diff suppressed because it is too large Load Diff

View File

@@ -3223,6 +3223,31 @@
| `end=(null)`| (initial)| `<null>` |
| `end=(undefined)`| (initial)| `<null>` |
## `enterKeyHint` (on `<input>` inside `<div>`)
| Test Case | Flags | Result |
| --- | --- | --- |
| `enterKeyHint=(string)`| (initial)| `<empty string>` |
| `enterKeyHint=(empty string)`| (initial)| `<empty string>` |
| `enterKeyHint=(array with string)`| (initial)| `<empty string>` |
| `enterKeyHint=(empty array)`| (initial)| `<empty string>` |
| `enterKeyHint=(object)`| (initial)| `<empty string>` |
| `enterKeyHint=(numeric string)`| (initial)| `<empty string>` |
| `enterKeyHint=(-1)`| (initial)| `<empty string>` |
| `enterKeyHint=(0)`| (initial)| `<empty string>` |
| `enterKeyHint=(integer)`| (initial)| `<empty string>` |
| `enterKeyHint=(NaN)`| (initial, warning)| `<empty string>` |
| `enterKeyHint=(float)`| (initial)| `<empty string>` |
| `enterKeyHint=(true)`| (initial, warning)| `<empty string>` |
| `enterKeyHint=(false)`| (initial, warning)| `<empty string>` |
| `enterKeyHint=(string 'true')`| (initial)| `<empty string>` |
| `enterKeyHint=(string 'false')`| (initial)| `<empty string>` |
| `enterKeyHint=(string 'on')`| (initial)| `<empty string>` |
| `enterKeyHint=(string 'off')`| (initial)| `<empty string>` |
| `enterKeyHint=(symbol)`| (initial, warning)| `<empty string>` |
| `enterKeyHint=(function)`| (initial, warning)| `<empty string>` |
| `enterKeyHint=(null)`| (initial)| `<empty string>` |
| `enterKeyHint=(undefined)`| (initial)| `<empty string>` |
## `exponent` (on `<feFuncA>` inside `<svg>`)
| Test Case | Flags | Result |
| --- | --- | --- |

View File

@@ -556,6 +556,11 @@ const attributes = [
tagName: 'animate',
read: getSVGAttribute('end'),
},
{
name: 'enterKeyHint',
tagName: 'input',
read: getProperty('enterKeyHint'),
},
{
name: 'exponent',
read: getSVGProperty('exponent'),

View File

@@ -208,6 +208,70 @@
return <ChildComponent customObject={new Custom()} />;
}
const baseInheritedKeys = Object.create(Object.prototype, {
enumerableStringBase: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('enumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableStringBase: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('nonEnumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
});
const inheritedKeys = Object.create(baseInheritedKeys, {
enumerableString: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableString: {
value: 3,
writable: true,
enumerable: false,
configurable: true,
},
123: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('nonEnumerableSymbol')]: {
value: 2,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('enumerableSymbol')]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
});
function InheritedKeys() {
return <ChildComponent data={inheritedKeys} />;
}
const object = {
string: "abc",
longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890",
@@ -294,6 +358,7 @@
<ObjectProps />
<UnserializableProps />
<CustomObject />
<InheritedKeys />
</Fragment>
);
}

View File

@@ -324,9 +324,9 @@ export default function Fibers({fibers, show, graphSettings, ...rest}) {
) : (
<small>Committed</small>
)}
{fiber.effectTag && [
{fiber.flags && [
<br key="br" />,
<small key="small">Effect: {fiber.effectTag}</small>,
<small key="small">Effect: {fiber.flags}</small>,
]}
</div>
</Vertex>,

View File

@@ -37,7 +37,7 @@ function getFriendlyTag(tag) {
}
}
function getFriendlyEffect(effectTag) {
function getFriendlyEffect(flags) {
const effects = {
1: 'Performed Work',
2: 'Placement',
@@ -49,7 +49,7 @@ function getFriendlyEffect(effectTag) {
128: 'Ref',
};
return Object.keys(effects)
.filter(flag => flag & effectTag)
.filter(flag => flag & flags)
.map(flag => effects[flag])
.join(' & ');
}
@@ -72,7 +72,7 @@ export default function describeFibers(rootFiber, workInProgress) {
...fiber,
id: id,
tag: getFriendlyTag(fiber.tag),
effectTag: getFriendlyEffect(fiber.effectTag),
flags: getFriendlyEffect(fiber.flags),
type: fiber.type && '<' + (fiber.type.name || fiber.type) + '>',
stateNode: `[${typeof fiber.stateNode}]`,
return: acknowledgeFiber(fiber.return),

2
fixtures/nesting/.env Normal file
View File

@@ -0,0 +1,2 @@
EXTEND_ESLINT=true
SKIP_PREFLIGHT_CHECK=true

View File

@@ -0,0 +1 @@
src/*/node_modules

27
fixtures/nesting/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# copies of shared
src/*/shared
src/*/node_modules

163
fixtures/nesting/README.md Normal file
View File

@@ -0,0 +1,163 @@
# Nested React Demo
This is a demo of how you can configure a build system to serve **two different versions of React** side by side in the same app. This is not optimal, and should only be used as a compromise to prevent your app from getting stuck on an old version of React.
## You Probably Don't Need This
Note that **this approach is meant to be an escape hatch, not the norm**.
Normally, we encourage you to use a single version of React across your whole app. When you need to upgrade React, it is better to try to upgrade it all at once. We try to keep breaking changes between versions to the minimum, and often there are automatic scripts ("codemods") that can assist you with migration. You can always find the migration information for any release on [our blog](https://reactjs.org/blog/).
Using a single version of React removes a lot of complexity. It is also essential to ensure the best experience for your users who don't have to download the code twice. Always prefer using one React.
## What Is This For?
However, for some apps that have been in production for many years, upgrading all screens at once may be prohibitively difficult. For example, React components written in 2014 may still rely on [the unofficial legacy context API](https://reactjs.org/docs/legacy-context.html) (not to be confused with the modern one), and are not always maintained.
Traditionally, this meant that if a legacy API is deprecated, you would be stuck on the old version of React forever. That prevents your whole app from receiving improvements and bugfixes. This repository demonstrates a hybrid approach. It shows how you can use a newer version of React for some parts of your app, while **lazy-loading an older version of React** for the parts that haven't been migrated yet.
This approach is inherently more complex, and should be used as a last resort when you can't upgrade.
## Version Requirements
This demo uses two different versions of React: React 17 for "modern" components (in `src/modern`), and React 16.8 for "legacy" components (in `src/legacy`).
**We still recommend upgrading your whole app to React 17 in one piece.** The React 17 release intentionally has minimal breaking changes so that it's easier to upgrade to. In particular, React 17 solves some problems with nesting related to event propagation that earlier versions of React did not handle well. We expect that this nesting demo may not be as useful today as during a future migration from React 17 to the future major versions where some of the long-deprecated APIs may be removed.
However, if you're already stuck on an old version of React, you may found this approach useful today. If you remove a Hook call from `src/shared/Clock.js`, you can downgrade the legacy React all the way down to React 16.3. If you then remove Context API usage from `src/legacy/createLegacyRoot.js`, you can further downgrade the legacy React version, but keep in mind that the usage of third-party libraries included in this demo (React Router and React Redux) may need to be adjusted or removed.
## Installation
To run this demo, open its folder in Terminal and execute:
```sh
npm install
npm start
```
If you want to test the production build, you can run instead:
```
npm install
npm run build
npx serve -s build
```
This sample app uses client-side routing and consists of two routes:
- `/` renders a page which uses a newer version of React. (In the production build, you can verify that only one version of React is being loaded when this route is rendered.)
- `/about` renders a page which uses an older version of React for a part of its tree. (In the production build, you can verify that both versions of React are loaded from different chunks.)
**The purpose of this demo is to show some nuances of such setup:**
- How to install two versions of React in a single app with npm side by side.
- How to avoid the ["invalid Hook call" error](https://github.com/facebook/react/issues/13991) while nesting React trees.
- How to pass context between different versions of React.
- How to lazy-load the second React bundle so it's only loaded on the screens that use it.
- How to do all of this without a special bundler configuration.
## How It Works
File structure is extremely important in this demo. It has a direct effect on which code is going to use which version of React. This particular demo is using Create React App without ejecting, so **it doesn't rely on any bundler plugins or configuration**. The principle of this demo is portable to other setups.
### Dependencies
We will use three different `package.json`s: one for non-React code at the root, and two in the respective `src/legacy` and `src/modern` folders that specify the React dependencies:
- **`package.json`**: The root `package.json` is a place for build dependencies (such as `react-scripts`) and any React-agnostic libraries (for example, `lodash`, `immer`, or `redux`). We do **not** include React or any React-related libraries in this file.
- **`src/legacy/package.json`**: This is where we declare the `react` and `react-dom` dependencies for the "legacy" trees. In this demo, we're using React 16.8 (although, as noted above, we could downgrade it further below). This is **also** where we specify any third-party libraries that use React. For example, we include `react-router` and `react-redux` in this example.
- **`src/modern/package.json`**: This is where we declare the `react` and `react-dom` dependencies for the "modern" trees. In this demo, we're using React 17. Here, we also specify third-party dependencies that use React and are used from the modern part of our app. This is why we *also* have `react-router` and `react-redux` in this file. (Their versions don't strictly have to match their `legacy` counterparts, but features that rely on context may require workarounds if they differ.)
The `scripts` in the root `package.json` are set up so that when you run `npm install` in it, it also runs `npm intall` in both `src/legacy` and `src/modern` folders.
**Note:** This demo is set up to use a few third-party dependencies (React Router and Redux). These are not essential, and you can remove them from the demo. They are included so we can show how to make them work with this approach.
### Folders
There are a few key folders in this example:
- **`src`**: Root of the source tree. At this level (or below it, except for the special folders noted below), you can put any logic that's agnostic of React. For example, in this demo we have `src/index.js` which is the app's entry point, and `src/store.js` which exports a Redux store. These regular modules only execute once, and are **not** duplicated between the bundles.
- **`src/legacy`**: This is where all the code using the older version of React should go. This includes React components and Hooks, and general product code that is **only** used by the legacy trees.
- **`src/modern`**: This is where all the code using the newer version of React should go. This includes React components and Hooks, and general product code that is **only** used by the modern trees.
- **`src/shared`**: You may have some components or Hooks that you wish to use from both modern and legacy subtrees. The build process is set up so that **everything inside `src/shared` gets copied by a file watcher** into both `src/legacy/shared` and `src/modern/shared` on every change. This lets you write a component or a Hook once, but reuse it in both places.
### Lazy Loading
Loading two Reacts on the same page is bad for the user experience, so you should strive to push this as far as possible from the critical path of your app. For example, if there is a dialog that is less commonly used, or a route that is rarely visited, those are better candidates for staying on an older version of React than parts of your homepage.
To encourage only loading the older React when necessary, this demo includes a helper that works similarly to `React.lazy`. For example, `src/modern/AboutPage.js`, simplified, looks like this:
```js
import lazyLegacyRoot from './lazyLegacyRoot';
// Lazy-load a component from the bundle using legacy React.
const Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));
function AboutPage() {
return (
<>
<h3>This component is rendered by React ({React.version}).</h3>
<Greeting />
</>
);
}
```
As a result, only if the `AboutPage` (and as a result, `<Greeting />`) gets rendered, we will load the bundle containing the legacy React and the legacy `Greeting` component. Like with `React.lazy()`, we wrap it in `<Suspense>` to specify the loading indicator:
```js
<Suspense fallback={<Spinner />}>
<AboutPage />
</Suspense>
```
If the legacy component is only rendered conditionally, we won't load the second React until it's shown:
```js
<>
<button onClick={() => setShowGreeting(true)}>
Say hi
</button>
{showGreeting && (
<Suspense fallback={<Spinner />}>
<Greeting />
</Suspense>
)}
</>
```
The implementation of the `src/modern/lazyLegacyRoot.js` helper is included so you can tweak it and customize it to your needs. Remember to test lazy loading with the production builds because the bundler may not optimize it in development.
### Context
If you have nested trees managed by different versions of React, the inner tree won't "see" the outer tree's Context.
This breaks third-party libraries like React Redux or React Router, as well as any of your own usage of Context (for example, for theming).
To solve this problem, we read all the Contexts we care about in the outer tree, pass them to the inner tree, and then wrap the inner tree in the corresponding Providers. You can see this in action in two files:
* `src/modern/lazyLegacyRoot.js`: Look for `useContext` calls, and how their results are combined into a single object that is passed through. **You can read more Contexts there** if your app requires them.
* `src/legacy/createLegacyRoot.js`: Look for the `Bridge` component which receives that object and wraps its children with the appropriate Context Providers. **You can wrap them with more Providers there** if your app requires them.
Note that, generally saying, this approach is somewhat fragile, especially because some libraries may not expose their Contexts officially or consider their structure private. You may be able to expose private Contexts by using a tool like [patch-package](https://www.npmjs.com/package/patch-package), but remember to keep all the versions pinned because even a patch release of a third-party library may change the behavior.
### Nesting Direction
In this demo, we use an older React inside an app managed by the newer React. However, we could rename the folders and apply the same approach in the other direction.
### Event Propagation
Note that before React 17, `event.stopPropagation()` in the inner React tree does not prevent the event propagation to the outer React tree. This may cause unexpected behavior when extracting a UI tree like a dialog to use a separate React. This is because prior to React 17, both Reacts would attach the event listener at the `document` level. React 17 fixes this by attaching handlers to the roots. We strongly recommend upgrading to React 17 before considering the nesting strategy for future upgrades.
### Gotchas
This setup is unusual, so it has a few gotchas.
* Don't add `package.json` to the `src/shared` folder. For example, if you want to use an npm React component inside `src/shared`, you should add it to both `src/modern/package.json` and `src/legacy/package.json` instead. You can use different versions of it but make sure your code works with both of them — and that it works with both Reacts!
* Don't use React outside of the `src/modern`, `src/legacy`, or `src/shared`. Don't add React-related libraries outside of `src/modern/package.json` or `src/legacy/package.json`.
* Remember that `src/shared` is where you write shared components, but the files you write there are automatically copied into `src/modern/shared` and `src/legacy/shared`, **from which you should import them**. Both of the target directories are in `.gitignore`. Importing directly from `src/shared` **will not work** because it is ambiguous what `react` refers to in that folder.
* Keep in mind that any code in `src/shared` gets duplicated between the legacy and the modern bundles. Code that should not be duplicated needs to be anywhere else in `src` (but you can't use React there since the version is ambiguous).
* You'll want to exclude `src/*/node_modules` from your linter's configuration, as this demo does in `.eslintignorerc`.
This setup is complicated, and we don't recommend it for most apps. However, we believe it is important to offer it as an option for apps that would otherwise get left behind. There might be ways to simplify it with a layer of tooling, but this example is intentionally showing the low-level mechanism that other tools may build on.

View File

@@ -0,0 +1,43 @@
{
"name": "react-nesting-example",
"version": "0.1.0",
"private": true,
"dependencies": {
"react-scripts": "3.4.1",
"redux": "^4.0.5"
},
"scripts": {
"postinstall": "run-p install:*",
"install:legacy": "cd src/legacy && npm install",
"install:modern": "cd src/modern && npm install",
"copy:legacy": "cpx 'src/shared/**' 'src/legacy/shared/'",
"copy:modern": "cpx 'src/shared/**' 'src/modern/shared/'",
"watch:legacy": "cpx 'src/shared/**' 'src/legacy/shared/' --watch --no-initial",
"watch:modern": "cpx 'src/shared/**' 'src/modern/shared/' --watch --no-initial",
"prebuild": "run-p copy:*",
"prestart": "run-p copy:*",
"start": "run-p start-app watch:*",
"start-app": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"cpx": "^1.5.0",
"npm-run-all": "^4.1.5"
}
}

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1 @@
import './modern/index';

View File

@@ -0,0 +1,51 @@
import React from 'react';
import {Component} from 'react';
import {findDOMNode} from 'react-dom';
import {Link} from 'react-router-dom';
import {connect} from 'react-redux';
import {store} from '../store';
import ThemeContext from './shared/ThemeContext';
import Clock from './shared/Clock';
store.subscribe(() => {
console.log('Counter:', store.getState());
});
class AboutSection extends Component {
componentDidMount() {
// The modern app is wrapped in StrictMode,
// but the legacy bits can still use old APIs.
findDOMNode(this);
}
render() {
return (
<ThemeContext.Consumer>
{theme => (
<div style={{border: '1px dashed black', padding: 20}}>
<h3>src/legacy/Greeting.js</h3>
<h4 style={{color: theme}}>
This component is rendered by the nested React ({React.version}).
</h4>
<Clock />
<p>
Counter: {this.props.counter}{' '}
<button onClick={() => this.props.dispatch({type: 'increment'})}>
+
</button>
</p>
<b>
<Link to="/">Go to Home</Link>
</b>
</div>
)}
</ThemeContext.Consumer>
);
}
}
function mapStateToProps(state) {
return {counter: state};
}
export default connect(mapStateToProps)(AboutSection);

View File

@@ -0,0 +1,43 @@
/* eslint-disable react/jsx-pascal-case */
import React from 'react';
import ReactDOM from 'react-dom';
import ThemeContext from './shared/ThemeContext';
// Note: this is a semi-private API, but it's ok to use it
// if we never inspect the values, and only pass them through.
import {__RouterContext} from 'react-router';
import {Provider} from 'react-redux';
// Pass through every context required by this tree.
// The context object is populated in src/modern/withLegacyRoot.
function Bridge({children, context}) {
return (
<ThemeContext.Provider value={context.theme}>
<__RouterContext.Provider value={context.router}>
{/*
If we used the newer react-redux@7.x in the legacy/package.json,
we woud instead import {ReactReduxContext} from 'react-redux'
and render <ReactReduxContext.Provider value={context.reactRedux}>.
*/}
<Provider store={context.reactRedux.store}>{children}</Provider>
</__RouterContext.Provider>
</ThemeContext.Provider>
);
}
export default function createLegacyRoot(container) {
return {
render(Component, props, context) {
ReactDOM.render(
<Bridge context={context}>
<Component {...props} />
</Bridge>,
container
);
},
unmount() {
ReactDOM.unmountComponentAtNode(container);
},
};
}

View File

@@ -0,0 +1,10 @@
{
"private": true,
"name": "react-nesting-example-legacy",
"dependencies": {
"react": "16.8",
"react-dom": "16.8",
"react-redux": "4.4.10",
"react-router-dom": "5.2.0"
}
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import {useContext} from 'react';
import {connect} from 'react-redux';
import ThemeContext from './shared/ThemeContext';
import lazyLegacyRoot from './lazyLegacyRoot';
// Lazy-load a component from the bundle using legacy React.
const Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));
function AboutPage({counter, dispatch}) {
const theme = useContext(ThemeContext);
return (
<>
<h2>src/modern/AboutPage.js</h2>
<h3 style={{color: theme}}>
This component is rendered by the outer React ({React.version}).
</h3>
<Greeting />
<br />
<p>
Counter: {counter}{' '}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</p>
</>
);
}
function mapStateToProps(state) {
return {counter: state};
}
export default connect(mapStateToProps)(AboutPage);

View File

@@ -0,0 +1,52 @@
import React from 'react';
import {useState, Suspense} from 'react';
import {BrowserRouter, Switch, Route} from 'react-router-dom';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import ThemeContext from './shared/ThemeContext';
export default function App() {
const [theme, setTheme] = useState('slategrey');
function handleToggleClick() {
if (theme === 'slategrey') {
setTheme('hotpink');
} else {
setTheme('slategrey');
}
}
return (
<BrowserRouter>
<ThemeContext.Provider value={theme}>
<div style={{fontFamily: 'sans-serif'}}>
<div
style={{
margin: 20,
padding: 20,
border: '1px solid black',
minHeight: 300,
}}>
<button onClick={handleToggleClick}>Toggle Theme Context</button>
<br />
<Suspense fallback={<Spinner />}>
<Switch>
<Route path="/about">
<AboutPage />
</Route>
<Route path="/">
<HomePage />
</Route>
</Switch>
</Suspense>
</div>
</div>
</ThemeContext.Provider>
</BrowserRouter>
);
}
function Spinner() {
return null;
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import {useContext} from 'react';
import {Link} from 'react-router-dom';
import ThemeContext from './shared/ThemeContext';
import Clock from './shared/Clock';
export default function HomePage({counter, dispatch}) {
const theme = useContext(ThemeContext);
return (
<>
<h2>src/modern/HomePage.js</h2>
<h3 style={{color: theme}}>
This component is rendered by the outer React ({React.version}).
</h3>
<Clock />
<b>
<Link to="/about">Go to About</Link>
</b>
</>
);
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import {StrictMode} from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import App from './App';
import {store} from '../store';
ReactDOM.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
document.getElementById('root')
);

View File

@@ -0,0 +1,94 @@
import React from 'react';
import {useContext, useMemo, useRef, useState, useLayoutEffect} from 'react';
import {__RouterContext} from 'react-router';
import {ReactReduxContext} from 'react-redux';
import ThemeContext from './shared/ThemeContext';
let rendererModule = {
status: 'pending',
promise: null,
result: null,
};
export default function lazyLegacyRoot(getLegacyComponent) {
let componentModule = {
status: 'pending',
promise: null,
result: null,
};
return function Wrapper(props) {
const createLegacyRoot = readModule(rendererModule, () =>
import('../legacy/createLegacyRoot')
).default;
const Component = readModule(componentModule, getLegacyComponent).default;
const containerRef = useRef(null);
const rootRef = useRef(null);
// Populate every contexts we want the legacy subtree to see.
// Then in src/legacy/createLegacyRoot we will apply them.
const theme = useContext(ThemeContext);
const router = useContext(__RouterContext);
const reactRedux = useContext(ReactReduxContext);
const context = useMemo(
() => ({
theme,
router,
reactRedux,
}),
[theme, router, reactRedux]
);
// Create/unmount.
useLayoutEffect(() => {
if (!rootRef.current) {
rootRef.current = createLegacyRoot(containerRef.current);
}
const root = rootRef.current;
return () => {
root.unmount();
};
}, [createLegacyRoot]);
// Mount/update.
useLayoutEffect(() => {
if (rootRef.current) {
rootRef.current.render(Component, props, context);
}
}, [Component, props, context]);
return <div style={{display: 'contents'}} ref={containerRef} />;
};
}
// This is similar to React.lazy, but implemented manually.
// We use this to Suspend rendering of this component until
// we fetch the component and the legacy React to render it.
function readModule(record, createPromise) {
if (record.status === 'fulfilled') {
return record.result;
}
if (record.status === 'rejected') {
throw record.result;
}
if (!record.promise) {
record.promise = createPromise().then(
value => {
if (record.status === 'pending') {
record.status = 'fulfilled';
record.promise = null;
record.result = value;
}
},
error => {
if (record.status === 'pending') {
record.status = 'rejected';
record.promise = null;
record.result = error;
}
}
);
}
throw record.promise;
}

View File

@@ -0,0 +1,10 @@
{
"private": true,
"name": "react-nesting-example-modern",
"dependencies": {
"react": "0.0.0-3d0895557",
"react-dom": "0.0.0-3d0895557",
"react-redux": "7.2.1",
"react-router-dom": "5.2.0"
}
}

View File

@@ -0,0 +1,8 @@
import React from 'react';
import useTime from './useTime';
export default function Clock() {
const time = useTime();
return <p>Time: {time}</p>;
}

View File

@@ -0,0 +1,5 @@
import {createContext} from 'react';
const ThemeContext = createContext(null);
export default ThemeContext;

View File

@@ -0,0 +1,12 @@
import {useState, useEffect} from 'react';
export default function useTimer() {
const [value, setValue] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => {
setValue(new Date());
}, 1000);
return () => clearInterval(id);
}, []);
return value.toLocaleTimeString();
}

View File

@@ -0,0 +1,14 @@
import {createStore} from 'redux';
function reducer(state = 0, action) {
switch (action.type) {
case 'increment':
return state + 1;
default:
return state;
}
}
// Because this file is declared above both Modern and Legacy folders,
// we can import this from either folder without duplicating the object.
export const store = createStore(reducer);

View File

@@ -28,12 +28,9 @@ export default class Chrome extends Component {
<div>
<ThemeToggleButton
onChange={theme => {
React.unstable_withSuspenseConfig(
() => {
this.setState({theme});
},
{timeoutMs: 6000}
);
React.startTransition(() => {
this.setState({theme});
});
}}
/>
</div>

View File

@@ -4,35 +4,36 @@
"packages/*"
],
"devDependencies": {
"@babel/cli": "^7.8.0",
"@babel/code-frame": "^7.8.0",
"@babel/core": "^7.8.0",
"@babel/helper-module-imports": "^7.8.0",
"@babel/parser": "^7.8.0",
"@babel/plugin-external-helpers": "^7.8.0",
"@babel/plugin-proposal-class-properties": "^7.8.0",
"@babel/plugin-proposal-object-rest-spread": "^7.8.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.0",
"@babel/plugin-syntax-jsx": "^7.8.0",
"@babel/plugin-transform-arrow-functions": "^7.8.0",
"@babel/plugin-transform-async-to-generator": "^7.8.0",
"@babel/plugin-transform-block-scoped-functions": "^7.8.0",
"@babel/plugin-transform-block-scoping": "^7.8.0",
"@babel/plugin-transform-classes": "^7.8.0",
"@babel/plugin-transform-computed-properties": "^7.8.0",
"@babel/plugin-transform-destructuring": "^7.8.0",
"@babel/plugin-transform-for-of": "^7.8.0",
"@babel/plugin-transform-literals": "^7.8.0",
"@babel/plugin-transform-modules-commonjs": "^7.8.0",
"@babel/plugin-transform-object-super": "^7.8.0",
"@babel/plugin-transform-parameters": "^7.8.0",
"@babel/plugin-transform-react-jsx-source": "^7.8.0",
"@babel/plugin-transform-shorthand-properties": "^7.8.0",
"@babel/plugin-transform-spread": "^7.8.0",
"@babel/plugin-transform-template-literals": "^7.8.0",
"@babel/preset-flow": "^7.8.0",
"@babel/preset-react": "^7.8.0",
"@babel/traverse": "^7.8.0",
"@babel/cli": "^7.10.5",
"@babel/code-frame": "^7.10.4",
"@babel/core": "^7.11.1",
"@babel/eslint-parser": "^7.11.4",
"@babel/helper-module-imports": "^7.10.4",
"@babel/parser": "^7.11.3",
"@babel/plugin-external-helpers": "^7.10.4",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.10.4",
"@babel/plugin-transform-arrow-functions": "^7.10.4",
"@babel/plugin-transform-async-to-generator": "^7.10.4",
"@babel/plugin-transform-block-scoped-functions": "^7.10.4",
"@babel/plugin-transform-block-scoping": "^7.11.1",
"@babel/plugin-transform-classes": "^7.10.4",
"@babel/plugin-transform-computed-properties": "^7.10.4",
"@babel/plugin-transform-destructuring": "^7.10.4",
"@babel/plugin-transform-for-of": "^7.10.4",
"@babel/plugin-transform-literals": "^7.10.4",
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
"@babel/plugin-transform-object-super": "^7.10.4",
"@babel/plugin-transform-parameters": "^7.10.5",
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
"@babel/plugin-transform-shorthand-properties": "^7.10.4",
"@babel/plugin-transform-spread": "^7.11.0",
"@babel/plugin-transform-template-literals": "^7.10.5",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/traverse": "^7.11.0",
"@mattiasbuelens/web-streams-polyfill": "^0.3.2",
"art": "0.10.1",
"babel-eslint": "^10.0.3",
@@ -46,13 +47,14 @@
"create-react-class": "^15.6.3",
"danger": "^9.2.10",
"error-stack-parser": "^2.0.6",
"eslint": "^7.0.0",
"eslint": "^7.7.0",
"eslint-config-fbjs": "^1.1.1",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-for-of-loops": "^1.0.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
"eslint-plugin-react": "^6.7.1",
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "1.2.0",
@@ -66,6 +68,7 @@
"jest": "^25.2.7",
"jest-cli": "^25.2.7",
"jest-diff": "^25.2.6",
"jest-environment-jsdom-sixteen": "^1.0.3",
"jest-snapshot-serializer-raw": "^1.1.0",
"minimatch": "^3.0.4",
"minimist": "^1.2.3",
@@ -101,7 +104,7 @@
},
"scripts": {
"build": "node ./scripts/rollup/build.js",
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer --type=NODE",
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh --type=NODE",
"linc": "node ./scripts/tasks/linc.js",
"lint": "node ./scripts/tasks/eslint.js",
"lint-build": "node ./scripts/rollup/validate/index.js",

View File

@@ -1,7 +1,7 @@
{
"name": "create-subscription",
"description": "utility for subscribing to external data sources inside React components",
"version": "16.13.1",
"version": "17.0.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
@@ -15,7 +15,7 @@
"cjs/"
],
"peerDependencies": {
"react": "^16.3.0"
"react": "^17.0.0"
},
"devDependencies": {
"rxjs": "^5.5.6"

View File

@@ -234,6 +234,10 @@ export function blur({relatedTarget} = {}) {
return new FocusEvent('blur', {relatedTarget});
}
export function focusOut({relatedTarget} = {}) {
return new FocusEvent('focusout', {relatedTarget, bubbles: true});
}
export function click(payload) {
return createMouseEvent('click', {
button: buttonType.primary,
@@ -259,6 +263,10 @@ export function focus({relatedTarget} = {}) {
return new FocusEvent('focus', {relatedTarget});
}
export function focusIn({relatedTarget} = {}) {
return new FocusEvent('focusin', {relatedTarget, bubbles: true});
}
export function scroll() {
return createEvent('scroll');
}

View File

@@ -22,12 +22,14 @@ const createEventTarget = node => ({
*/
blur(payload) {
node.dispatchEvent(domEvents.blur(payload));
node.dispatchEvent(domEvents.focusOut(payload));
},
click(payload) {
node.dispatchEvent(domEvents.click(payload));
},
focus(payload) {
node.dispatchEvent(domEvents.focus(payload));
node.dispatchEvent(domEvents.focusIn(payload));
node.focus();
},
keydown(payload) {

View File

@@ -1,3 +1,30 @@
## 4.1.2
* Fix a crash with the TypeScript 4.x parser. ([@eps1lon](https://github.com/eps1lon) in [#19815](https://github.com/facebook/react/pull/19815))
## 4.1.1
* Improve support for optional chaining. ([@pfongkye](https://github.com/pfongkye) in [#19680](https://github.com/facebook/react/pull/19680))
* Fix a false positive warning for TypeScript parameters. ([@NeoRaider](https://github.com/NeoRaider) in [#19751](https://github.com/facebook/react/pull/19751))
## 4.1.0
* **New Violations:** Warn when dependencies change on every render. ([@captbaritone](https://github.com/captbaritone) in [#19590](https://github.com/facebook/react/pull/19590))
## 4.0.8
* Fixes TypeScript `typeof` annotation to not be considered a dependency. ([@delca85](https://github.com/delca85) in [#19316](https://github.com/facebook/react/pull/19316))
## 4.0.7
* Fixes an overly coarse dependency suggestion. ([@gaearon](https://github.com/gaearon) in [#19313](https://github.com/facebook/react/pull/19313))
## 4.0.6
* Fix crashes and other bugs related to optional chaining. ([@gaearon](https://github.com/gaearon) in [#19273](https://github.com/facebook/react/pull/19273) and [#19275](https://github.com/facebook/react/pull/19275))
## 4.0.5
* Fix a crash when the dependency array has an empty element. ([@yeonjuan](https://github.com/yeonjuan) in [#19145](https://github.com/facebook/react/pull/19145))
* Fix a false positive warning that occurs with optional chaining. ([@fredvollmer](https://github.com/fredvollmer) in [#19061](https://github.com/facebook/react/pull/19061))
## 4.0.4
* Fix a false positive warning that occurs with optional chaining. ([@fredvollmer](https://github.com/fredvollmer) in [#19061](https://github.com/facebook/react/pull/19061))
* Support nullish coalescing and optional chaining. ([@yanneves](https://github.com/yanneves) in [#19008](https://github.com/facebook/react/pull/19008))
## 4.0.3
* Remove the heuristic that checks all Hooks ending with `Effect` due to too many false positives. ([@gaearon](https://github.com/gaearon) in [#19004](https://github.com/facebook/react/pull/19004))

View File

@@ -752,9 +752,7 @@ const tests = {
errors: [
conditionalError('useState'),
conditionalError('useState'),
// TODO: ideally this *should* warn, but ESLint
// doesn't plan full support for ?? until it advances.
// conditionalError('useState'),
conditionalError('useState'),
],
},
{

View File

@@ -1,7 +1,7 @@
{
"name": "eslint-plugin-react-hooks",
"description": "ESLint rules for React Hooks",
"version": "4.0.5",
"version": "4.2.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
@@ -32,6 +32,8 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
},
"devDependencies": {
"@typescript-eslint/parser": "^2.26.0"
"@typescript-eslint/parser-v2": "npm:@typescript-eslint/parser@^2.26.0",
"@typescript-eslint/parser-v3": "npm:@typescript-eslint/parser@^3.10.0",
"@typescript-eslint/parser-v4": "npm:@typescript-eslint/parser@^4.1.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@
"homepage": "https://reactjs.org/",
"peerDependencies": {
"jest": "^23.0.1 || ^24.0.0 || ^25.1.0",
"scheduler": "^0.15.0"
"scheduler": "^0.20.0"
},
"files": [
"LICENSE",

View File

@@ -1,6 +1,6 @@
{
"name": "jest-react",
"version": "0.11.1",
"version": "0.12.0",
"description": "Jest matchers and utilities for testing React components.",
"main": "index.js",
"repository": {
@@ -20,8 +20,8 @@
"homepage": "https://reactjs.org/",
"peerDependencies": {
"jest": "^23.0.1 || ^24.0.0 || ^25.1.0",
"react": "^16.0.0",
"react-test-renderer": "^16.0.0"
"react": "^17.0.0",
"react-test-renderer": "^17.0.0"
},
"dependencies": {
"object-assign": "^4.1.1"

View File

@@ -1,7 +1,7 @@
{
"name": "react-art",
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
"version": "16.13.1",
"version": "17.0.0",
"main": "index.js",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"create-react-class": "^15.6.2",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"scheduler": "^0.19.0"
"scheduler": "^0.20.0"
},
"peerDependencies": {
"react": "^16.13.0"
"react": "17.0.0"
},
"files": [
"LICENSE",

View File

@@ -10,10 +10,6 @@ import Mode from 'art/modes/current';
import invariant from 'shared/invariant';
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
import type {
ReactEventResponder,
ReactEventResponderInstance,
} from 'shared/ReactTypes';
const pooledTransform = new Transform();
@@ -429,22 +425,6 @@ export function clearContainer(container) {
// TODO Implement this
}
export function DEPRECATED_mountResponderInstance(
responder: ReactEventResponder<any, any>,
responderInstance: ReactEventResponderInstance<any, any>,
props: Object,
state: Object,
instance: Object,
) {
throw new Error('Not yet implemented.');
}
export function DEPRECATED_unmountResponderInstance(
responderInstance: ReactEventResponderInstance<any, any>,
): void {
throw new Error('Not yet implemented.');
}
export function getFundamentalComponentInstance(fundamentalInstance) {
throw new Error('Not yet implemented.');
}
@@ -469,10 +449,6 @@ export function getInstanceFromNode(node) {
throw new Error('Not yet implemented.');
}
export function removeInstanceEventHandles(instance) {
// noop
}
export function isOpaqueHydratingObject(value: mixed): boolean {
throw new Error('Not yet implemented');
}

View File

@@ -17,6 +17,6 @@
"umd/"
],
"peerDependencies": {
"react": "^16.3.0-alpha.1"
"react": "^17.0.0"
}
}

View File

@@ -24,7 +24,7 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^17.0.0"
},
"dependencies": {
"loose-envify": "^1.1.0",

View File

@@ -26,7 +26,7 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^17.0.0"
},
"dependencies": {
"error-stack-parser": "^2.0.2",

View File

@@ -13,8 +13,6 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
ReactProviderType,
ReactEventResponder,
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {
Fiber,
@@ -22,7 +20,6 @@ import type {
} from 'react-reconciler/src/ReactInternalTypes';
import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';
import ErrorStackParser from 'error-stack-parser';
@@ -64,10 +61,6 @@ type Hook = {
next: Hook | null,
};
type TimeoutConfig = {|
timeoutMs: number,
|};
function getPrimitiveStackCache(): Map<string, Array<any>> {
// This initializes a cache of all primitive hooks so that the top
// most stack frames added by calling the primitive hook can be removed.
@@ -86,16 +79,6 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useDebugValue(null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
Dispatcher.useMutableSource(
{
_source: {},
_getVersion: () => 1,
_workInProgressVersionPrimary: null,
_workInProgressVersionSecondary: null,
},
() => null,
() => () => {},
);
} finally {
readHookLog = hookLog;
hookLog = [];
@@ -270,25 +253,7 @@ function useMutableSource<Source, Snapshot>(
return value;
}
function useResponder(
responder: ReactEventResponder<any, any>,
listenerProps: Object,
): ReactEventResponderListener<any, any> {
// Don't put the actual event responder object in, just its displayName
const value = {
responder: responder.displayName || 'EventResponder',
props: listenerProps,
};
hookLog.push({primitive: 'Responder', stackError: new Error(), value});
return {
responder,
props: listenerProps,
};
}
function useTransition(
config: SuspenseConfig | null | void,
): [(() => void) => void, boolean] {
function useTransition(): [(() => void) => void, boolean] {
// useTransition() composes multiple hooks internally.
// Advance the current hook index the same number of times
// so that subsequent hooks have the right memoized state.
@@ -297,12 +262,12 @@ function useTransition(
hookLog.push({
primitive: 'Transition',
stackError: new Error(),
value: config,
value: undefined,
});
return [callback => {}, false];
}
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
function useDeferredValue<T>(value: T): T {
// useDeferredValue() composes multiple hooks internally.
// Advance the current hook index the same number of times
// so that subsequent hooks have the right memoized state.
@@ -345,7 +310,6 @@ const Dispatcher: DispatcherType = {
useReducer,
useRef,
useState,
useResponder,
useTransition,
useMutableSource,
useDeferredValue,

View File

@@ -1,46 +0,0 @@
/**
* Copyright (c) Facebook, 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.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactDebugTools;
describe('ReactHooksInspection', () => {
beforeEach(() => {
jest.resetModules();
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
React = require('react');
ReactDebugTools = require('react-debug-tools');
});
// @gate experimental
it('should inspect a simple useResponder hook', () => {
const TestResponder = React.DEPRECATED_createResponder('TestResponder', {});
function Foo(props) {
const listener = React.DEPRECATED_useResponder(TestResponder, {
preventDefault: false,
});
return <div DEPRECATED_flareListeners={listener}>Hello world</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: 0,
name: 'Responder',
value: {props: {preventDefault: false}, responder: 'TestResponder'},
subHooks: [],
},
]);
});
});

View File

@@ -284,7 +284,7 @@ describe('ReactHooksInspection', () => {
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
expect(getterCalls).toBe(1);

View File

@@ -22,7 +22,7 @@ describe('ReactHooksInspectionIntegration', () => {
React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
act = ReactTestRenderer.act;
act = ReactTestRenderer.unstable_concurrentAct;
ReactDebugTools = require('react-debug-tools');
});
@@ -787,7 +787,7 @@ describe('ReactHooksInspectionIntegration', () => {
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
expect(getterCalls).toBe(1);
@@ -848,9 +848,9 @@ describe('ReactHooksInspectionIntegration', () => {
if (__EXPERIMENTAL__) {
it('should support composite useMutableSource hook', () => {
const mutableSource = React.createMutableSource({}, () => 1);
const mutableSource = React.unstable_createMutableSource({}, () => 1);
function Foo(props) {
React.useMutableSource(
React.unstable_useMutableSource(
mutableSource,
() => 'snapshot',
() => {},

View File

@@ -21,6 +21,7 @@ Be sure to run this function *before* importing e.g. `react`, `react-dom`, `reac
The `config` object may contain:
* `host: string` (defaults to "localhost") - Websocket will connect to this host.
* `port: number` (defaults to `8097`) - Websocket will connect to this port.
* `useHttps: boolean` (defaults to `false`) - Websocked should use a secure protocol (wss).
* `websocket: Websocket` - Custom websocket to use. Overrides `host` and `port` settings if provided.
* `resolveRNStyle: (style: number) => ?Object` - Used by the React Native style plug-in.
* `isAppActive: () => boolean` - If provided, DevTools will poll this method and wait until it returns true before connecting to React.
@@ -38,6 +39,24 @@ require("react-devtools-core/standalone")
.startServer(port);
```
Renders DevTools interface into a DOM node over SSL using a custom host name (Default is localhost).
```js
const host = 'dev.server.com';
const options = {
key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};
require("react-devtools-core/standalone")
.setContentDOMNode(document.getElementById("container"))
.setStatusListener(status => {
// This callback is optional...
})
.startServer(port, host, options);
```
Reference the `react-devtools` package for a complete integration example.
## Development

View File

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

View File

@@ -24,6 +24,7 @@ type ConnectOptions = {
host?: string,
nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
port?: number,
useHttps?: boolean,
resolveRNStyle?: ResolveNativeStyle,
isAppActive?: () => boolean,
websocket?: ?WebSocket,
@@ -55,12 +56,14 @@ export function connectToDevTools(options: ?ConnectOptions) {
const {
host = 'localhost',
nativeStyleEditorValidAttributes,
useHttps = false,
port = 8097,
websocket,
resolveRNStyle = null,
isAppActive = () => true,
} = options || {};
const protocol = useHttps ? 'wss' : 'ws';
let retryTimeoutID: TimeoutID | null = null;
function scheduleRetry() {
@@ -80,7 +83,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
let bridge: BackendBridge | null = null;
const messageListeners = [];
const uri = 'ws://' + host + ':' + port;
const uri = protocol + '://' + host + ':' + port;
// If existing websocket is passed, use it.
// This is necessary to support our custom integrations.

View File

@@ -242,8 +242,20 @@ function connectToSocket(socket: WebSocket) {
};
}
function startServer(port?: number = 8097) {
const httpServer = require('http').createServer();
type ServerOptions = {
key?: string,
cert?: string,
};
function startServer(
port?: number = 8097,
host?: string = 'localhost',
httpsOptions?: ServerOptions,
) {
const useHttps = !!httpsOptions;
const httpServer = useHttps
? require('https').createServer(httpsOptions)
: require('http').createServer();
const server = new Server({server: httpServer});
let connected: WebSocket | null = null;
server.on('connection', (socket: WebSocket) => {
@@ -298,7 +310,9 @@ function startServer(port?: number = 8097) {
'\n;' +
backendFile.toString() +
'\n;' +
'ReactDevToolsBackend.connectToDevTools();',
`ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
useHttps ? 'true' : 'false'
}});`,
);
});

View File

@@ -17,9 +17,14 @@ const __DEV__ = NODE_ENV === 'development';
const DEVTOOLS_VERSION = getVersionString();
// This targets RN/Hermes.
process.env.BABEL_CONFIG_ADDITIONAL_TARGETS = JSON.stringify({
ie: '11',
});
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
entry: {
backend: './src/backend.js',
},

View File

@@ -19,7 +19,7 @@ const DEVTOOLS_VERSION = getVersionString();
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
target: 'electron-main',
entry: {
standalone: './src/standalone.js',

View File

@@ -2,8 +2,8 @@
"manifest_version": 2,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "4.7.0",
"version_name": "4.7.0",
"version": "4.8.2",
"version_name": "4.8.2",
"minimum_chrome_version": "49",
@@ -32,7 +32,14 @@
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/renderer.js"
"build/renderer.js",
"build/background.js.map",
"build/contentScript.js.map",
"build/injectGlobalHook.js.map",
"build/main.js.map",
"build/panel.js.map",
"build/renderer.js.map",
"build/react_devtools_backend.js.map"
],
"background": {

View File

@@ -2,8 +2,8 @@
"manifest_version": 2,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "4.7.0",
"version_name": "4.7.0",
"version": "4.8.2",
"version_name": "4.8.2",
"minimum_chrome_version": "49",
@@ -32,7 +32,14 @@
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/renderer.js"
"build/renderer.js",
"build/background.js.map",
"build/contentScript.js.map",
"build/injectGlobalHook.js.map",
"build/main.js.map",
"build/panel.js.map",
"build/renderer.js.map",
"build/react_devtools_backend.js.map"
],
"background": {

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.7.0",
"version": "4.8.2",
"applications": {
"gecko": {
@@ -37,7 +37,14 @@
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/renderer.js"
"build/renderer.js",
"build/background.js.map",
"build/contentScript.js.map",
"build/injectGlobalHook.js.map",
"build/main.js.map",
"build/panel.js.map",
"build/renderer.js.map",
"build/react_devtools_backend.js.map"
],
"background": {

View File

@@ -18,13 +18,13 @@
"test:edge": "node ./edge/test"
},
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-transform-flow-strip-types": "^7.1.6",
"@babel/plugin-transform-react-jsx-source": "^7.2.0",
"@babel/preset-env": "^7.1.6",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"archiver": "^3.0.0",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^9.0.0",
@@ -44,5 +44,8 @@
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
},
"dependencies": {
"web-ext": "^4"
}
}

View File

@@ -11,6 +11,7 @@ document.addEventListener('DOMContentLoaded', function() {
const location = ln.href;
ln.onclick = function() {
chrome.tabs.create({active: true, url: location});
return false;
};
})();
}

View File

@@ -72,7 +72,7 @@ function setup(hook) {
initBackend(hook, agent, window);
// Let the frontend know that the backend has attached listeners and is ready for messages.
// This covers the case of of syncing saved values after reloading/navigating while DevTools remain open.
// 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.

View File

@@ -98,18 +98,7 @@ chrome.runtime.onMessage.addListener((request, sender) => {
// We use browserAction instead of pageAction because this lets us
// display a custom default popup when React is *not* detected.
// It is specified in the manifest.
let reactBuildType = request.reactBuildType;
if (sender.url.indexOf('facebook.github.io/react') !== -1) {
// Cheat: We use the development version on the website because
// it is better for interactive examples. However we're going
// to get misguided bug reports if the extension highlights it
// as using the dev version. We're just going to special case
// our own documentation and cheat. It is acceptable to use dev
// version of React in React docs, but not in any other case.
reactBuildType = 'production';
}
setIconAndPopup(reactBuildType, sender.tab.id);
setIconAndPopup(request.reactBuildType, sender.tab.id);
}
}
});

View File

@@ -296,7 +296,7 @@ function createPanelIfReactLoaded() {
let needsToSyncElementSelection = false;
chrome.devtools.panels.create(
isChrome ? '⚛ Components' : 'Components',
isChrome ? '⚛ Components' : 'Components',
'',
'panel.html',
extensionPanel => {
@@ -326,7 +326,7 @@ function createPanelIfReactLoaded() {
);
chrome.devtools.panels.create(
isChrome ? '⚛ Profiler' : 'Profiler',
isChrome ? '⚛ Profiler' : 'Profiler',
'',
'panel.html',
extensionPanel => {

View File

@@ -1,3 +1,10 @@
/**
* Copyright (c) Facebook, 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.
*/
const {execSync} = require('child_process');
const {readFileSync} = require('fs');
const {resolve} = require('path');

View File

@@ -2,7 +2,6 @@
const {resolve} = require('path');
const {DefinePlugin} = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const {GITHUB_URL, getVersionString} = require('./utils');
const NODE_ENV = process.env.NODE_ENV;
@@ -19,7 +18,7 @@ const DEVTOOLS_VERSION = getVersionString();
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
entry: {
backend: './src/backend.js',
},
@@ -41,14 +40,7 @@ module.exports = {
},
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {drop_debugger: false},
output: {comments: true},
},
}),
],
minimize: false,
},
plugins: [
new DefinePlugin({

View File

@@ -18,7 +18,7 @@ const DEVTOOLS_VERSION = getVersionString();
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map',
entry: {
background: './src/background.js',
contentScript: './src/contentScript.js',
@@ -44,6 +44,9 @@ module.exports = {
scheduler: resolve(builtModulesDir, 'scheduler'),
},
},
optimization: {
minimize: false,
},
plugins: [
new DefinePlugin({
__DEV__: false,

View File

@@ -8,7 +8,7 @@ This is a low-level package. If you're looking for the standalone DevTools app,
This package exports two entry points: a frontend (to be run in the main `window`) and a backend (to be installed and run within an `iframe`<sup>1</sup>).
The frontend and backend can be initialized in any order, but **the backend must not be activated until after the frontend has been initialized**. Because of this, the simplest sequence is:
The frontend and backend can be initialized in any order, but **the backend must not be activated until the frontend initialization has completed**. Because of this, the simplest sequence is:
1. Frontend (DevTools interface) initialized in the main `window`.
1. Backend initialized in an `iframe`.

View File

@@ -1,6 +1,6 @@
{
"name": "react-devtools-inline",
"version": "4.7.0",
"version": "4.8.2",
"description": "Embed react-devtools within a website",
"license": "MIT",
"main": "./dist/backend.js",
@@ -22,13 +22,13 @@
},
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-transform-flow-strip-types": "^7.1.6",
"@babel/plugin-transform-react-jsx-source": "^7.2.0",
"@babel/preset-env": "^7.1.6",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.4",

View File

@@ -1,6 +1,5 @@
const {resolve} = require('path');
const {DefinePlugin} = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const {
GITHUB_URL,
getVersionString,
@@ -18,7 +17,7 @@ const DEVTOOLS_VERSION = getVersionString();
module.exports = {
mode: __DEV__ ? 'development' : 'production',
devtool: false,
devtool: __DEV__ ? 'eval-cheap-source-map' : 'source-map',
entry: {
backend: './src/backend.js',
frontend: './src/frontend.js',
@@ -38,14 +37,7 @@ module.exports = {
scheduler: 'scheduler',
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {drop_debugger: false},
output: {comments: true},
},
}),
],
minimize: false,
},
plugins: [
new DefinePlugin({

View File

@@ -0,0 +1,15 @@
# Experimental React Concurrent Mode Profiler
https://react-devtools-scheduling-profiler.vercel.app/
## Setting up continuous deployment with CircleCI and Vercel
These instructions are intended for internal use, but may be useful if you are setting up a custom production deployment of the scheduling profiler.
1. Create a Vercel token at https://vercel.com/account/tokens.
2. Configure CircleCI:
1. In CircleCI, navigate to the repository's Project Settings.
2. In the Advanced tab, ensure that "Pass secrets to builds from forked pull requests" is set to false.
3. In the Environment Variables tab, add the Vercel token as a new `SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN` environment variable.
The Vercel project will be created when the deploy job runs.

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) Facebook, 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.
*/
const {execSync} = require('child_process');
const {readFileSync} = require('fs');
const {resolve} = require('path');
function getGitCommit() {
try {
return execSync('git show -s --format=%h')
.toString()
.trim();
} catch (error) {
// Mozilla runs this command from a git archive.
// In that context, there is no Git revision.
return null;
}
}
function getVersionString() {
const packageVersion = JSON.parse(
readFileSync(resolve(__dirname, './package.json')),
).version;
const commit = getGitCommit();
return `${packageVersion}-${commit}`;
}
module.exports = {
getVersionString,
};

View File

@@ -0,0 +1,35 @@
{
"private": true,
"name": "react-devtools-scheduling-profiler",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"build": "cross-env NODE_ENV=production cross-env TARGET=remote webpack --config webpack.config.js",
"start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
},
"dependencies": {
"@elg/speedscope": "1.9.0-a6f84db",
"clipboard-js": "^0.3.6",
"memoize-one": "^5.1.1",
"nullthrows": "^1.1.1",
"pretty-ms": "^7.0.0",
"react-virtualized-auto-sizer": "^1.0.2",
"regenerator-runtime": "^0.13.7"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.1",
"@reach/menu-button": "^0.11.2",
"@reach/tooltip": "^0.11.2",
"babel-loader": "^8.1.0",
"css-loader": "^4.2.1",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.3.0",
"style-loader": "^1.2.1",
"url-loader": "^4.1.0",
"vercel": "^20.1.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"worker-loader": "^3.0.2"
}
}

View File

@@ -0,0 +1,19 @@
.DevTools {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-background);
color: var(--color-text);
}
.TabContent {
flex: 1 1 100%;
overflow: auto;
-webkit-app-region: no-drag;
}
.DevTools, .DevTools * {
box-sizing: border-box;
-webkit-font-smoothing: var(--font-smoothing);
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
// Reach styles need to come before any component styles.
// This makes overriding the styles simpler.
import '@reach/menu-button/styles.css';
import '@reach/tooltip/styles.css';
import * as React from 'react';
import {SchedulingProfiler} from './SchedulingProfiler';
import {useBrowserTheme, useDisplayDensity} from './hooks';
import styles from './App.css';
import 'react-devtools-shared/src/devtools/views/root.css';
export default function App() {
useBrowserTheme();
useDisplayDensity();
return (
<div className={styles.DevTools}>
<div className={styles.TabContent}>
<SchedulingProfiler />
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
.CanvasPage {
position: absolute;
top: 0.5rem;
bottom: 0.5rem;
left: 0.5rem;
right: 0.5rem;
}

View File

@@ -0,0 +1,479 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {
Point,
HorizontalPanAndZoomViewOnChangeCallback,
} from './view-base';
import type {
ReactHoverContextInfo,
ReactProfilerData,
ReactMeasure,
} from './types';
import * as React from 'react';
import {
Fragment,
useEffect,
useLayoutEffect,
useRef,
useState,
useCallback,
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import {copy} from 'clipboard-js';
import prettyMilliseconds from 'pretty-ms';
import {
HorizontalPanAndZoomView,
ResizableSplitView,
Surface,
VerticalScrollView,
View,
createComposedLayout,
lastViewTakesUpRemainingSpaceLayout,
useCanvasInteraction,
verticallyStackedLayout,
zeroPoint,
} from './view-base';
import {
FlamechartView,
ReactEventsView,
ReactMeasuresView,
TimeAxisMarkersView,
UserTimingMarksView,
} from './content-views';
import {COLORS} from './content-views/constants';
import EventTooltip from './EventTooltip';
import ContextMenu from './context/ContextMenu';
import ContextMenuItem from './context/ContextMenuItem';
import useContextMenu from './context/useContextMenu';
import {getBatchRange} from './utils/getBatchRange';
import styles from './CanvasPage.css';
const CONTEXT_MENU_ID = 'canvas';
type Props = {|
profilerData: ReactProfilerData,
|};
function CanvasPage({profilerData}: Props) {
return (
<div
className={styles.CanvasPage}
style={{backgroundColor: COLORS.BACKGROUND}}>
<AutoSizer>
{({height, width}: {height: number, width: number}) => (
<AutoSizedCanvas data={profilerData} height={height} width={width} />
)}
</AutoSizer>
</div>
);
}
const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
const {batchUID, duration, timestamp, type} = measure;
const [startTime, stopTime] = getBatchRange(batchUID, data);
copy(
JSON.stringify({
type,
timestamp: prettyMilliseconds(timestamp),
duration: prettyMilliseconds(duration),
batchDuration: prettyMilliseconds(stopTime - startTime),
}),
);
};
const zoomToBatch = (
data: ReactProfilerData,
measure: ReactMeasure,
syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
) => {
const {batchUID} = measure;
const [startTime, stopTime] = getBatchRange(batchUID, data);
syncedHorizontalPanAndZoomViews.forEach(syncedView =>
// Using time as range works because the views' intrinsic content size is
// based on time.
syncedView.zoomToRange(startTime, stopTime),
);
};
type AutoSizedCanvasProps = {|
data: ReactProfilerData,
height: number,
width: number,
|};
function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
const [mouseLocation, setMouseLocation] = useState<Point>(zeroPoint); // DOM coordinates
const [
hoveredEvent,
setHoveredEvent,
] = useState<ReactHoverContextInfo | null>(null);
const surfaceRef = useRef(new Surface());
const userTimingMarksViewRef = useRef(null);
const reactEventsViewRef = useRef(null);
const reactMeasuresViewRef = useRef(null);
const flamechartViewRef = useRef(null);
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
[],
);
useLayoutEffect(() => {
const surface = surfaceRef.current;
const defaultFrame = {origin: zeroPoint, size: {width, height}};
// Clear synced views
syncedHorizontalPanAndZoomViewsRef.current = [];
const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
newState,
triggeringView?: HorizontalPanAndZoomView,
) => {
syncedHorizontalPanAndZoomViewsRef.current.forEach(
syncedView =>
triggeringView !== syncedView && syncedView.setScrollState(newState),
);
};
// Top content
const topContentStack = new View(
surface,
defaultFrame,
verticallyStackedLayout,
);
const axisMarkersView = new TimeAxisMarkersView(
surface,
defaultFrame,
data.duration,
);
topContentStack.addSubview(axisMarkersView);
if (data.otherUserTimingMarks.length > 0) {
const userTimingMarksView = new UserTimingMarksView(
surface,
defaultFrame,
data.otherUserTimingMarks,
data.duration,
);
userTimingMarksViewRef.current = userTimingMarksView;
topContentStack.addSubview(userTimingMarksView);
}
const reactEventsView = new ReactEventsView(surface, defaultFrame, data);
reactEventsViewRef.current = reactEventsView;
topContentStack.addSubview(reactEventsView);
const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
surface,
defaultFrame,
topContentStack,
data.duration,
syncAllHorizontalPanAndZoomViewStates,
);
syncedHorizontalPanAndZoomViewsRef.current.push(
topContentHorizontalPanAndZoomView,
);
// Resizable content
const reactMeasuresView = new ReactMeasuresView(
surface,
defaultFrame,
data,
);
reactMeasuresViewRef.current = reactMeasuresView;
const reactMeasuresVerticalScrollView = new VerticalScrollView(
surface,
defaultFrame,
reactMeasuresView,
);
const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
surface,
defaultFrame,
reactMeasuresVerticalScrollView,
data.duration,
syncAllHorizontalPanAndZoomViewStates,
);
syncedHorizontalPanAndZoomViewsRef.current.push(
reactMeasuresHorizontalPanAndZoomView,
);
const flamechartView = new FlamechartView(
surface,
defaultFrame,
data.flamechart,
data.duration,
);
flamechartViewRef.current = flamechartView;
const flamechartVerticalScrollView = new VerticalScrollView(
surface,
defaultFrame,
flamechartView,
);
const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
surface,
defaultFrame,
flamechartVerticalScrollView,
data.duration,
syncAllHorizontalPanAndZoomViewStates,
);
syncedHorizontalPanAndZoomViewsRef.current.push(
flamechartHorizontalPanAndZoomView,
);
const resizableContentStack = new ResizableSplitView(
surface,
defaultFrame,
reactMeasuresHorizontalPanAndZoomView,
flamechartHorizontalPanAndZoomView,
);
const rootView = new View(
surface,
defaultFrame,
createComposedLayout(
verticallyStackedLayout,
lastViewTakesUpRemainingSpaceLayout,
),
);
rootView.addSubview(topContentHorizontalPanAndZoomView);
rootView.addSubview(resizableContentStack);
surfaceRef.current.rootView = rootView;
}, [data]);
useLayoutEffect(() => {
if (canvasRef.current) {
surfaceRef.current.setCanvas(canvasRef.current, {width, height});
}
}, [width, height]);
const interactor = useCallback(interaction => {
if (canvasRef.current === null) {
return;
}
surfaceRef.current.handleInteraction(interaction);
// Defer drawing to canvas until React's commit phase, to avoid drawing
// twice and to ensure that both the canvas and DOM elements managed by
// React are in sync.
setMouseLocation({
x: interaction.payload.event.x,
y: interaction.payload.event.y,
});
}, []);
useCanvasInteraction(canvasRef, interactor);
useContextMenu({
data: {
data,
hoveredEvent,
},
id: CONTEXT_MENU_ID,
onChange: setIsContextMenuShown,
ref: canvasRef,
});
useEffect(() => {
const {current: userTimingMarksView} = userTimingMarksViewRef;
if (userTimingMarksView) {
userTimingMarksView.onHover = userTimingMark => {
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
setHoveredEvent({
userTimingMark,
event: null,
flamechartStackFrame: null,
measure: null,
data,
});
}
};
}
const {current: reactEventsView} = reactEventsViewRef;
if (reactEventsView) {
reactEventsView.onHover = event => {
if (!hoveredEvent || hoveredEvent.event !== event) {
setHoveredEvent({
userTimingMark: null,
event,
flamechartStackFrame: null,
measure: null,
data,
});
}
};
}
const {current: reactMeasuresView} = reactMeasuresViewRef;
if (reactMeasuresView) {
reactMeasuresView.onHover = measure => {
if (!hoveredEvent || hoveredEvent.measure !== measure) {
setHoveredEvent({
userTimingMark: null,
event: null,
flamechartStackFrame: null,
measure,
data,
});
}
};
}
const {current: flamechartView} = flamechartViewRef;
if (flamechartView) {
flamechartView.setOnHover(flamechartStackFrame => {
if (
!hoveredEvent ||
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
) {
setHoveredEvent({
userTimingMark: null,
event: null,
flamechartStackFrame,
measure: null,
data,
});
}
});
}
}, [
hoveredEvent,
data, // Attach onHover callbacks when views are re-created on data change
]);
useLayoutEffect(() => {
const {current: userTimingMarksView} = userTimingMarksViewRef;
if (userTimingMarksView) {
userTimingMarksView.setHoveredMark(
hoveredEvent ? hoveredEvent.userTimingMark : null,
);
}
const {current: reactEventsView} = reactEventsViewRef;
if (reactEventsView) {
reactEventsView.setHoveredEvent(hoveredEvent ? hoveredEvent.event : null);
}
const {current: reactMeasuresView} = reactMeasuresViewRef;
if (reactMeasuresView) {
reactMeasuresView.setHoveredMeasure(
hoveredEvent ? hoveredEvent.measure : null,
);
}
const {current: flamechartView} = flamechartViewRef;
if (flamechartView) {
flamechartView.setHoveredFlamechartStackFrame(
hoveredEvent ? hoveredEvent.flamechartStackFrame : null,
);
}
}, [hoveredEvent]);
// Draw to canvas in React's commit phase
useLayoutEffect(() => {
surfaceRef.current.displayIfNeeded();
});
return (
<Fragment>
<canvas ref={canvasRef} height={height} width={width} />
<ContextMenu id={CONTEXT_MENU_ID}>
{contextData => {
if (contextData.hoveredEvent == null) {
return null;
}
const {
event,
flamechartStackFrame,
measure,
} = contextData.hoveredEvent;
return (
<Fragment>
{event !== null && (
<ContextMenuItem
onClick={() => copy(event.componentName)}
title="Copy component name">
Copy component name
</ContextMenuItem>
)}
{event !== null && (
<ContextMenuItem
onClick={() => copy(event.componentStack)}
title="Copy component stack">
Copy component stack
</ContextMenuItem>
)}
{measure !== null && (
<ContextMenuItem
onClick={() =>
zoomToBatch(
contextData.data,
measure,
syncedHorizontalPanAndZoomViewsRef.current,
)
}
title="Zoom to batch">
Zoom to batch
</ContextMenuItem>
)}
{measure !== null && (
<ContextMenuItem
onClick={() => copySummary(contextData.data, measure)}
title="Copy summary">
Copy summary
</ContextMenuItem>
)}
{flamechartStackFrame !== null && (
<ContextMenuItem
onClick={() => copy(flamechartStackFrame.scriptUrl)}
title="Copy file path">
Copy file path
</ContextMenuItem>
)}
{flamechartStackFrame !== null && (
<ContextMenuItem
onClick={() =>
copy(
`line ${flamechartStackFrame.locationLine ??
''}, column ${flamechartStackFrame.locationColumn ??
''}`,
)
}
title="Copy location">
Copy location
</ContextMenuItem>
)}
</Fragment>
);
}}
</ContextMenu>
{!isContextMenuShown && (
<EventTooltip
data={data}
hoveredEvent={hoveredEvent}
origin={mouseLocation}
/>
)}
</Fragment>
);
}
export default CanvasPage;

View File

@@ -0,0 +1,78 @@
.Tooltip {
position: fixed;
display: inline-block;
border-radius: 0.125rem;
max-width: 300px;
padding: 0.25rem;
user-select: none;
pointer-events: none;
background-color: #ffffff;
border: 1px solid #ccc;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
font-size: 11px;
}
.Divider {
height: 1px;
background-color: #aaa;
margin: 0.5rem 0;
}
.DetailsGrid {
display: grid;
padding-top: 5px;
grid-gap: 2px 5px;
grid-template-columns: min-content auto;
}
.DetailsGridLabel {
color: #666;
text-align: right;
}
.DetailsGridURL {
word-break: break-all;
max-height: 50vh;
overflow: hidden;
}
.FlamechartStackFrameName {
word-break: break-word;
margin-left: 0.4rem;
}
.ComponentName {
font-weight: bold;
word-break: break-word;
margin-right: 0.4rem;
}
.ComponentStack {
overflow: hidden;
max-width: 35em;
max-height: 10em;
margin: 0;
font-size: 0.9em;
line-height: 1.5;
-webkit-mask-image: linear-gradient(
180deg,
#fff,
#fff 5em,
transparent
);
mask-image: linear-gradient(
180deg,
#fff,
#fff 5em,
transparent
);
white-space: pre;
}
.ReactMeasureLabel {
margin-left: 0.4rem;
}
.UserTimingLabel {
word-break: break-word;
}

View File

@@ -0,0 +1,292 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {Point} from './view-base';
import type {
FlamechartStackFrame,
ReactEvent,
ReactHoverContextInfo,
ReactMeasure,
ReactProfilerData,
Return,
UserTimingMark,
} from './types';
import * as React from 'react';
import {Fragment, useRef} from 'react';
import prettyMilliseconds from 'pretty-ms';
import {COLORS} from './content-views/constants';
import {getBatchRange} from './utils/getBatchRange';
import useSmartTooltip from './utils/useSmartTooltip';
import styles from './EventTooltip.css';
type Props = {|
data: ReactProfilerData,
hoveredEvent: ReactHoverContextInfo | null,
origin: Point,
|};
function formatTimestamp(ms) {
return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms';
}
function formatDuration(ms) {
return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3});
}
function trimmedString(string: string, length: number): string {
if (string.length > length) {
return `${string.substr(0, length - 1)}`;
}
return string;
}
function getReactEventLabel(type): string | null {
switch (type) {
case 'schedule-render':
return 'render scheduled';
case 'schedule-state-update':
return 'state update scheduled';
case 'schedule-force-update':
return 'force update scheduled';
case 'suspense-suspend':
return 'suspended';
case 'suspense-resolved':
return 'suspense resolved';
case 'suspense-rejected':
return 'suspense rejected';
default:
return null;
}
}
function getReactEventColor(event: ReactEvent): string | null {
switch (event.type) {
case 'schedule-render':
return COLORS.REACT_SCHEDULE_HOVER;
case 'schedule-state-update':
case 'schedule-force-update':
return event.isCascading
? COLORS.REACT_SCHEDULE_CASCADING_HOVER
: COLORS.REACT_SCHEDULE_HOVER;
case 'suspense-suspend':
case 'suspense-resolved':
case 'suspense-rejected':
return COLORS.REACT_SUSPEND_HOVER;
default:
return null;
}
}
function getReactMeasureLabel(type): string | null {
switch (type) {
case 'commit':
return 'commit';
case 'render-idle':
return 'idle';
case 'render':
return 'render';
case 'layout-effects':
return 'layout effects';
case 'passive-effects':
return 'passive effects';
default:
return null;
}
}
export default function EventTooltip({data, hoveredEvent, origin}: Props) {
const tooltipRef = useSmartTooltip({
mouseX: origin.x,
mouseY: origin.y,
});
if (hoveredEvent === null) {
return null;
}
const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent;
if (event !== null) {
return <TooltipReactEvent event={event} tooltipRef={tooltipRef} />;
} else if (measure !== null) {
return (
<TooltipReactMeasure
data={data}
measure={measure}
tooltipRef={tooltipRef}
/>
);
} else if (flamechartStackFrame !== null) {
return (
<TooltipFlamechartNode
stackFrame={flamechartStackFrame}
tooltipRef={tooltipRef}
/>
);
} else if (userTimingMark !== null) {
return (
<TooltipUserTimingMark mark={userTimingMark} tooltipRef={tooltipRef} />
);
}
return null;
}
function formatComponentStack(componentStack: string): string {
const lines = componentStack.split('\n').map(line => line.trim());
lines.shift();
if (lines.length > 5) {
return lines.slice(0, 5).join('\n') + '\n...';
}
return lines.join('\n');
}
const TooltipFlamechartNode = ({
stackFrame,
tooltipRef,
}: {
stackFrame: FlamechartStackFrame,
tooltipRef: Return<typeof useRef>,
}) => {
const {
name,
timestamp,
duration,
scriptUrl,
locationLine,
locationColumn,
} = stackFrame;
return (
<div className={styles.Tooltip} ref={tooltipRef}>
{formatDuration(duration)}
<span className={styles.FlamechartStackFrameName}>{name}</span>
<div className={styles.DetailsGrid}>
<div className={styles.DetailsGridLabel}>Timestamp:</div>
<div>{formatTimestamp(timestamp)}</div>
{scriptUrl && (
<>
<div className={styles.DetailsGridLabel}>Script URL:</div>
<div className={styles.DetailsGridURL}>{scriptUrl}</div>
</>
)}
{(locationLine !== undefined || locationColumn !== undefined) && (
<>
<div className={styles.DetailsGridLabel}>Location:</div>
<div>
line {locationLine}, column {locationColumn}
</div>
</>
)}
</div>
</div>
);
};
const TooltipReactEvent = ({
event,
tooltipRef,
}: {
event: ReactEvent,
tooltipRef: Return<typeof useRef>,
}) => {
const label = getReactEventLabel(event.type);
const color = getReactEventColor(event);
if (!label || !color) {
if (__DEV__) {
console.warn('Unexpected event type "%s"', event.type);
}
return null;
}
const {componentName, componentStack, timestamp} = event;
return (
<div className={styles.Tooltip} ref={tooltipRef}>
{componentName && (
<span className={styles.ComponentName} style={{color}}>
{trimmedString(componentName, 768)}
</span>
)}
{label}
<div className={styles.Divider} />
<div className={styles.DetailsGrid}>
<div className={styles.DetailsGridLabel}>Timestamp:</div>
<div>{formatTimestamp(timestamp)}</div>
{componentStack && (
<Fragment>
<div className={styles.DetailsGridLabel}>Component stack:</div>
<pre className={styles.ComponentStack}>
{formatComponentStack(componentStack)}
</pre>
</Fragment>
)}
</div>
</div>
);
};
const TooltipReactMeasure = ({
data,
measure,
tooltipRef,
}: {
data: ReactProfilerData,
measure: ReactMeasure,
tooltipRef: Return<typeof useRef>,
}) => {
const label = getReactMeasureLabel(measure.type);
if (!label) {
if (__DEV__) {
console.warn('Unexpected measure type "%s"', measure.type);
}
return null;
}
const {batchUID, duration, timestamp, lanes} = measure;
const [startTime, stopTime] = getBatchRange(batchUID, data);
return (
<div className={styles.Tooltip} ref={tooltipRef}>
{formatDuration(duration)}
<span className={styles.ReactMeasureLabel}>{label}</span>
<div className={styles.Divider} />
<div className={styles.DetailsGrid}>
<div className={styles.DetailsGridLabel}>Timestamp:</div>
<div>{formatTimestamp(timestamp)}</div>
<div className={styles.DetailsGridLabel}>Batch duration:</div>
<div>{formatDuration(stopTime - startTime)}</div>
<div className={styles.DetailsGridLabel}>
Lane{lanes.length === 1 ? '' : 's'}:
</div>
<div>{lanes.join(', ')}</div>
</div>
</div>
);
};
const TooltipUserTimingMark = ({
mark,
tooltipRef,
}: {
mark: UserTimingMark,
tooltipRef: Return<typeof useRef>,
}) => {
const {name, timestamp} = mark;
return (
<div className={styles.Tooltip} ref={tooltipRef}>
<span className={styles.UserTimingLabel}>{name}</span>
<div className={styles.Divider} />
<div className={styles.DetailsGrid}>
<div className={styles.DetailsGridLabel}>Timestamp:</div>
<div>{formatTimestamp(timestamp)}</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,10 @@
/**
* https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications
*/
.Input {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import * as React from 'react';
import {useCallback, useRef} from 'react';
import Button from 'react-devtools-shared/src/devtools/views/Button';
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import styles from './ImportButton.css';
type Props = {|
onFileSelect: (file: File) => void,
|};
export default function ImportButton({onFileSelect}: Props) {
const inputRef = useRef<HTMLInputElement | null>(null);
const handleFiles = useCallback(() => {
const input = inputRef.current;
if (input === null) {
return;
}
if (input.files.length > 0) {
onFileSelect(input.files[0]);
}
// Reset input element to allow the same file to be re-imported
input.value = '';
}, [onFileSelect]);
const uploadData = useCallback(() => {
if (inputRef.current !== null) {
inputRef.current.click();
}
}, []);
return (
<>
<input
ref={inputRef}
className={styles.Input}
type="file"
onChange={handleFiles}
tabIndex={-1}
/>
<Button onClick={uploadData} title="Load profile...">
<ButtonIcon type="import" />
</Button>
</>
);
}

View File

@@ -0,0 +1,108 @@
.SchedulingProfiler {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
font-family: var(--font-family-sans);
font-size: var(--font-size-sans-normal);
background-color: var(--color-background);
color: var(--color-text);
}
.SchedulingProfiler, .SchedulingProfiler * {
box-sizing: border-box;
-webkit-font-smoothing: var(--font-smoothing);
}
.Content {
position: relative;
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.Paragraph {
text-align: center;
}
.ErrorMessage {
margin: 0.5rem 0;
color: var(--color-dim);
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
}
.Row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-flow: wrap;
}
.EmptyStateContainer {
text-align: center;
}
.Header {
font-size: var(--font-size-sans-large);
margin-bottom: 0.5rem;
}
.Toolbar {
height: 2.25rem;
padding: 0 0.25rem;
flex: 0 0 auto;
display: flex;
align-items: center;
border-bottom: 1px solid var(--color-border);
}
.VRule {
height: 20px;
width: 1px;
border-left: 1px solid var(--color-border);
padding-left: 0.25rem;
margin-left: 0.25rem;
}
.Spacer {
flex: 1;
}
.Link {
color: var(--color-button);
}
.ScreenshotWrapper {
max-width: 30rem;
padding: 0 1rem;
margin-bottom: 2rem;
}
.Screenshot {
width: 100%;
border-radius: 0.4em;
border: 2px solid var(--color-border);
}
.AppName {
font-size: var(--font-size-sans-large);
margin-right: 0.5rem;
user-select: none;
}
@media screen and (max-width: 350px) {
.AppName {
display: none;
}
}
@media screen and (max-height: 600px) {
.ScreenshotWrapper {
display: none;
}
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {Resource} from 'react-devtools-shared/src/devtools/cache';
import type {ReactProfilerData} from './types';
import type {ImportWorkerOutputData} from './import-worker/import.worker';
import * as React from 'react';
import {Suspense, useCallback, useState} from 'react';
import {createResource} from 'react-devtools-shared/src/devtools/cache';
import ReactLogo from 'react-devtools-shared/src/devtools/views/ReactLogo';
import ImportButton from './ImportButton';
import CanvasPage from './CanvasPage';
import ImportWorker from './import-worker/import.worker';
import profilerBrowser from './assets/profilerBrowser.png';
import styles from './SchedulingProfiler.css';
type DataResource = Resource<void, File, ReactProfilerData | Error>;
function createDataResourceFromImportedFile(file: File): DataResource {
return createResource(
() => {
return new Promise<ReactProfilerData | Error>((resolve, reject) => {
const worker: Worker = new (ImportWorker: any)();
worker.onmessage = function(event) {
const data = ((event.data: any): ImportWorkerOutputData);
switch (data.status) {
case 'SUCCESS':
resolve(data.processedData);
break;
case 'INVALID_PROFILE_ERROR':
resolve(data.error);
break;
case 'UNEXPECTED_ERROR':
reject(data.error);
break;
}
worker.terminate();
};
worker.postMessage({file});
});
},
() => file,
{useWeakMap: true},
);
}
export function SchedulingProfiler(_: {||}) {
const [dataResource, setDataResource] = useState<DataResource | null>(null);
const handleFileSelect = useCallback((file: File) => {
setDataResource(createDataResourceFromImportedFile(file));
}, []);
return (
<div className={styles.SchedulingProfiler}>
<div className={styles.Toolbar}>
<ReactLogo />
<span className={styles.AppName}>Concurrent Mode Profiler</span>
<div className={styles.VRule} />
<ImportButton onFileSelect={handleFileSelect} />
<div className={styles.Spacer} />
</div>
<div className={styles.Content}>
{dataResource ? (
<Suspense fallback={<ProcessingData />}>
<DataResourceComponent
dataResource={dataResource}
onFileSelect={handleFileSelect}
/>
</Suspense>
) : (
<Welcome onFileSelect={handleFileSelect} />
)}
</div>
</div>
);
}
const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
<div className={styles.EmptyStateContainer}>
<div className={styles.ScreenshotWrapper}>
<img
src={profilerBrowser}
className={styles.Screenshot}
alt="Profiler screenshot"
/>
</div>
<div className={styles.Header}>Welcome!</div>
<div className={styles.Row}>
Click the import button
<ImportButton onFileSelect={onFileSelect} /> to import a Chrome
performance profile.
</div>
</div>
);
const ProcessingData = () => (
<div className={styles.EmptyStateContainer}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);
const CouldNotLoadProfile = ({error, onFileSelect}) => (
<div className={styles.EmptyStateContainer}>
<div className={styles.Header}>Could not load profile</div>
{error.message && (
<div className={styles.Row}>
<div className={styles.ErrorMessage}>{error.message}</div>
</div>
)}
<div className={styles.Row}>
Try importing
<ImportButton onFileSelect={onFileSelect} />
another Chrome performance profile.
</div>
</div>
);
const DataResourceComponent = ({
dataResource,
onFileSelect,
}: {|
dataResource: DataResource,
onFileSelect: (file: File) => void,
|}) => {
const dataOrError = dataResource.read();
if (dataOrError instanceof Error) {
return (
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
);
}
return <CanvasPage profilerData={dataOrError} />;
};

View File

@@ -7,4 +7,4 @@
* @flow
*/
export * from './src/dom/Tap';
export const enableDarkMode = false;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -7,4 +7,9 @@
* @flow
*/
export * from './src/dom/Input';
export {
COMFORTABLE_LINE_HEIGHT,
COMPACT_LINE_HEIGHT,
} from 'react-devtools-shared/src/constants.js';
export const REACT_TOTAL_NUM_LANES = 31;

View File

@@ -0,0 +1,378 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {
Flamechart,
FlamechartStackFrame,
FlamechartStackLayer,
} from '../types';
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
import {
ColorView,
Surface,
View,
layeredLayout,
rectContainsPoint,
rectEqualToRect,
intersectionOfRects,
rectIntersectsRect,
verticallyStackedLayout,
} from '../view-base';
import {
durationToWidth,
positioningScaleFactor,
timestampToPosition,
} from './utils/positioning';
import {
COLORS,
FLAMECHART_FONT_SIZE,
FLAMECHART_FRAME_HEIGHT,
FLAMECHART_TEXT_PADDING,
COLOR_HOVER_DIM_DELTA,
BORDER_SIZE,
} from './constants';
import {ColorGenerator, dimmedColor, hslaColorToString} from './utils/colors';
// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399
const colorGenerator = new ColorGenerator(
{min: 30, max: 330},
{min: 50, max: 80, count: 3},
85,
);
colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100});
function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) {
return colorGenerator.colorForID(scriptUrl ?? '');
}
function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string {
const color = defaultHslaColorForStackFrame(stackFrame);
return hslaColorToString(color);
}
function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
const color = dimmedColor(
defaultHslaColorForStackFrame(stackFrame),
COLOR_HOVER_DIM_DELTA,
);
return hslaColorToString(color);
}
const cachedFlamechartTextWidths = new Map();
const trimFlamechartText = (
context: CanvasRenderingContext2D,
text: string,
width: number,
) => {
for (let i = text.length - 1; i >= 0; i--) {
const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
if (measuredWidth == null) {
measuredWidth = context.measureText(trimmedText).width;
cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
}
if (measuredWidth <= width) {
return trimmedText;
}
}
return null;
};
class FlamechartStackLayerView extends View {
/** Layer to display */
_stackLayer: FlamechartStackLayer;
/** A set of `stackLayer`'s frames, for efficient lookup. */
_stackFrameSet: Set<FlamechartStackFrame>;
_intrinsicSize: Size;
_hoveredStackFrame: FlamechartStackFrame | null = null;
_onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
constructor(
surface: Surface,
frame: Rect,
stackLayer: FlamechartStackLayer,
duration: number,
) {
super(surface, frame);
this._stackLayer = stackLayer;
this._stackFrameSet = new Set(stackLayer);
this._intrinsicSize = {
width: duration,
height: FLAMECHART_FRAME_HEIGHT,
};
}
desiredSize() {
return this._intrinsicSize;
}
setHoveredFlamechartStackFrame(
hoveredStackFrame: FlamechartStackFrame | null,
) {
if (this._hoveredStackFrame === hoveredStackFrame) {
return; // We're already hovering over this frame
}
// Only care about frames displayed by this view.
const stackFrameToSet =
hoveredStackFrame && this._stackFrameSet.has(hoveredStackFrame)
? hoveredStackFrame
: null;
if (this._hoveredStackFrame === stackFrameToSet) {
return; // Resulting state is unchanged
}
this._hoveredStackFrame = stackFrameToSet;
this.setNeedsDisplay();
}
draw(context: CanvasRenderingContext2D) {
const {
frame,
_stackLayer,
_hoveredStackFrame,
_intrinsicSize,
visibleArea,
} = this;
context.fillStyle = COLORS.BACKGROUND;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y,
visibleArea.size.width,
visibleArea.size.height,
);
context.textAlign = 'left';
context.textBaseline = 'middle';
context.font = `${FLAMECHART_FONT_SIZE}px sans-serif`;
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
for (let i = 0; i < _stackLayer.length; i++) {
const stackFrame = _stackLayer[i];
const {name, timestamp, duration} = stackFrame;
const width = durationToWidth(duration, scaleFactor);
if (width < 1) {
continue; // Too small to render at this zoom level
}
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
const nodeRect: Rect = {
origin: {x, y: frame.origin.y},
size: {
width: Math.floor(width - BORDER_SIZE),
height: Math.floor(FLAMECHART_FRAME_HEIGHT - BORDER_SIZE),
},
};
if (!rectIntersectsRect(nodeRect, visibleArea)) {
continue; // Not in view
}
const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];
context.fillStyle = showHoverHighlight
? hoverColorForStackFrame(stackFrame)
: defaultColorForStackFrame(stackFrame);
const drawableRect = intersectionOfRects(nodeRect, visibleArea);
context.fillRect(
drawableRect.origin.x,
drawableRect.origin.y,
drawableRect.size.width,
drawableRect.size.height,
);
if (width > FLAMECHART_TEXT_PADDING * 2) {
const trimmedName = trimFlamechartText(
context,
name,
width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0),
);
if (trimmedName !== null) {
context.fillStyle = COLORS.PRIORITY_LABEL;
// Prevent text from being drawn outside `viewableArea`
const textOverflowsViewableArea = !rectEqualToRect(
drawableRect,
nodeRect,
);
if (textOverflowsViewableArea) {
context.save();
context.beginPath();
context.rect(
drawableRect.origin.x,
drawableRect.origin.y,
drawableRect.size.width,
drawableRect.size.height,
);
context.closePath();
context.clip();
}
context.fillText(
trimmedName,
nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0),
nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
);
if (textOverflowsViewableArea) {
context.restore();
}
}
}
}
}
/**
* @private
*/
_handleMouseMove(interaction: MouseMoveInteraction) {
const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this;
const {location} = interaction.payload;
if (!_onHover || !rectContainsPoint(location, visibleArea)) {
return;
}
// Find the node being hovered over.
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
let startIndex = 0;
let stopIndex = _stackLayer.length - 1;
while (startIndex <= stopIndex) {
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
const flamechartStackFrame = _stackLayer[currentIndex];
const {timestamp, duration} = flamechartStackFrame;
const width = durationToWidth(duration, scaleFactor);
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
if (x <= location.x && x + width >= location.x) {
_onHover(flamechartStackFrame);
return;
}
if (x > location.x) {
stopIndex = currentIndex - 1;
} else {
startIndex = currentIndex + 1;
}
}
_onHover(null);
}
handleInteraction(interaction: Interaction) {
switch (interaction.type) {
case 'mousemove':
this._handleMouseMove(interaction);
break;
}
}
}
export class FlamechartView extends View {
_flamechartRowViews: FlamechartStackLayerView[] = [];
/** Container view that vertically stacks flamechart rows */
_verticalStackView: View;
_hoveredStackFrame: FlamechartStackFrame | null = null;
_onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
constructor(
surface: Surface,
frame: Rect,
flamechart: Flamechart,
duration: number,
) {
super(surface, frame, layeredLayout);
this.setDataAndUpdateSubviews(flamechart, duration);
}
setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) {
const {surface, frame, _onHover, _hoveredStackFrame} = this;
// Clear existing rows on data update
if (this._verticalStackView) {
this.removeAllSubviews();
this._flamechartRowViews = [];
}
this._verticalStackView = new View(surface, frame, verticallyStackedLayout);
this._flamechartRowViews = flamechart.map(stackLayer => {
const rowView = new FlamechartStackLayerView(
surface,
frame,
stackLayer,
duration,
);
this._verticalStackView.addSubview(rowView);
// Update states
rowView._onHover = _onHover;
rowView.setHoveredFlamechartStackFrame(_hoveredStackFrame);
return rowView;
});
// Add a plain background view to prevent gaps from appearing between
// flamechartRowViews.
const colorView = new ColorView(surface, frame, COLORS.BACKGROUND);
this.addSubview(colorView);
this.addSubview(this._verticalStackView);
}
setHoveredFlamechartStackFrame(
hoveredStackFrame: FlamechartStackFrame | null,
) {
this._hoveredStackFrame = hoveredStackFrame;
this._flamechartRowViews.forEach(rowView =>
rowView.setHoveredFlamechartStackFrame(hoveredStackFrame),
);
}
setOnHover(onHover: (node: FlamechartStackFrame | null) => void) {
this._onHover = onHover;
this._flamechartRowViews.forEach(rowView => (rowView._onHover = onHover));
}
desiredSize() {
// Ignore the wishes of the background color view
return this._verticalStackView.desiredSize();
}
/**
* @private
*/
_handleMouseMove(interaction: MouseMoveInteraction) {
const {_onHover, visibleArea} = this;
if (!_onHover) {
return;
}
const {location} = interaction.payload;
if (!rectContainsPoint(location, visibleArea)) {
// Clear out any hovered flamechart stack frame
_onHover(null);
}
}
handleInteraction(interaction: Interaction) {
switch (interaction.type) {
case 'mousemove':
this._handleMouseMove(interaction);
break;
}
}
}

View File

@@ -0,0 +1,276 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {ReactEvent, ReactProfilerData} from '../types';
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
import {
positioningScaleFactor,
timestampToPosition,
positionToTimestamp,
widthToDuration,
} from './utils/positioning';
import {
View,
Surface,
rectContainsPoint,
rectIntersectsRect,
intersectionOfRects,
} from '../view-base';
import {
COLORS,
EVENT_ROW_PADDING,
EVENT_DIAMETER,
BORDER_SIZE,
} from './constants';
const EVENT_ROW_HEIGHT_FIXED =
EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
function isSuspenseEvent(event: ReactEvent): boolean %checks {
return (
event.type === 'suspense-suspend' ||
event.type === 'suspense-resolved' ||
event.type === 'suspense-rejected'
);
}
export class ReactEventsView extends View {
_profilerData: ReactProfilerData;
_intrinsicSize: Size;
_hoveredEvent: ReactEvent | null = null;
onHover: ((event: ReactEvent | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
super(surface, frame);
this._profilerData = profilerData;
this._intrinsicSize = {
width: this._profilerData.duration,
height: EVENT_ROW_HEIGHT_FIXED,
};
}
desiredSize() {
return this._intrinsicSize;
}
setHoveredEvent(hoveredEvent: ReactEvent | null) {
if (this._hoveredEvent === hoveredEvent) {
return;
}
this._hoveredEvent = hoveredEvent;
this.setNeedsDisplay();
}
/**
* Draw a single `ReactEvent` as a circle in the canvas.
*/
_drawSingleReactEvent(
context: CanvasRenderingContext2D,
rect: Rect,
event: ReactEvent,
baseY: number,
scaleFactor: number,
showHoverHighlight: boolean,
) {
const {frame} = this;
const {timestamp, type} = event;
const x = timestampToPosition(timestamp, scaleFactor, frame);
const radius = EVENT_DIAMETER / 2;
const eventRect: Rect = {
origin: {
x: x - radius,
y: baseY,
},
size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER},
};
if (!rectIntersectsRect(eventRect, rect)) {
return; // Not in view
}
let fillStyle = null;
switch (type) {
case 'schedule-render':
case 'schedule-state-update':
case 'schedule-force-update':
if (event.isCascading) {
fillStyle = showHoverHighlight
? COLORS.REACT_SCHEDULE_CASCADING_HOVER
: COLORS.REACT_SCHEDULE_CASCADING;
} else {
fillStyle = showHoverHighlight
? COLORS.REACT_SCHEDULE_HOVER
: COLORS.REACT_SCHEDULE;
}
break;
case 'suspense-suspend':
case 'suspense-resolved':
case 'suspense-rejected':
fillStyle = showHoverHighlight
? COLORS.REACT_SUSPEND_HOVER
: COLORS.REACT_SUSPEND;
break;
default:
if (__DEV__) {
console.warn('Unexpected event type "%s"', type);
}
break;
}
if (fillStyle !== null) {
const y = eventRect.origin.y + radius;
context.beginPath();
context.fillStyle = fillStyle;
context.arc(x, y, radius, 0, 2 * Math.PI);
context.fill();
}
}
draw(context: CanvasRenderingContext2D) {
const {
frame,
_profilerData: {events},
_hoveredEvent,
visibleArea,
} = this;
context.fillStyle = COLORS.BACKGROUND;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y,
visibleArea.size.width,
visibleArea.size.height,
);
// Draw events
const baseY = frame.origin.y + EVENT_ROW_PADDING;
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);
const highlightedEvents: ReactEvent[] = [];
events.forEach(event => {
if (
event === _hoveredEvent ||
(_hoveredEvent &&
isSuspenseEvent(event) &&
isSuspenseEvent(_hoveredEvent) &&
event.id === _hoveredEvent.id)
) {
highlightedEvents.push(event);
return;
}
this._drawSingleReactEvent(
context,
visibleArea,
event,
baseY,
scaleFactor,
false,
);
});
// Draw the highlighted items on top so they stand out.
// This is helpful if there are multiple (overlapping) items close to each other.
highlightedEvents.forEach(event => {
this._drawSingleReactEvent(
context,
visibleArea,
event,
baseY,
scaleFactor,
true,
);
});
// Render bottom border.
// Propose border rect, check if intersects with `rect`, draw intersection.
const borderFrame: Rect = {
origin: {
x: frame.origin.x,
y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
},
size: {
width: frame.size.width,
height: BORDER_SIZE,
},
};
if (rectIntersectsRect(borderFrame, visibleArea)) {
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
context.fillStyle = COLORS.PRIORITY_BORDER;
context.fillRect(
borderDrawableRect.origin.x,
borderDrawableRect.origin.y,
borderDrawableRect.size.width,
borderDrawableRect.size.height,
);
}
}
/**
* @private
*/
_handleMouseMove(interaction: MouseMoveInteraction) {
const {frame, onHover, visibleArea} = this;
if (!onHover) {
return;
}
const {location} = interaction.payload;
if (!rectContainsPoint(location, visibleArea)) {
onHover(null);
return;
}
const {
_profilerData: {events},
} = this;
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
const eventTimestampAllowance = widthToDuration(
EVENT_DIAMETER / 2,
scaleFactor,
);
// Because data ranges may overlap, we want to find the last intersecting item.
// This will always be the one on "top" (the one the user is hovering over).
for (let index = events.length - 1; index >= 0; index--) {
const event = events[index];
const {timestamp} = event;
if (
timestamp - eventTimestampAllowance <= hoverTimestamp &&
hoverTimestamp <= timestamp + eventTimestampAllowance
) {
onHover(event);
return;
}
}
onHover(null);
}
handleInteraction(interaction: Interaction) {
switch (interaction.type) {
case 'mousemove':
this._handleMouseMove(interaction);
break;
}
}
}

View File

@@ -0,0 +1,318 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {ReactLane, ReactMeasure, ReactProfilerData} from '../types';
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
import {
durationToWidth,
positioningScaleFactor,
positionToTimestamp,
timestampToPosition,
} from './utils/positioning';
import {
View,
Surface,
rectContainsPoint,
rectIntersectsRect,
intersectionOfRects,
} from '../view-base';
import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
import {REACT_TOTAL_NUM_LANES} from '../constants';
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
function getMeasuresForLane(
allMeasures: ReactMeasure[],
lane: ReactLane,
): ReactMeasure[] {
return allMeasures.filter(measure => measure.lanes.includes(lane));
}
export class ReactMeasuresView extends View {
_profilerData: ReactProfilerData;
_intrinsicSize: Size;
_lanesToRender: ReactLane[];
_laneToMeasures: Map<ReactLane, ReactMeasure[]>;
_hoveredMeasure: ReactMeasure | null = null;
onHover: ((measure: ReactMeasure | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
super(surface, frame);
this._profilerData = profilerData;
this._performPreflightComputations();
}
_performPreflightComputations() {
this._lanesToRender = [];
this._laneToMeasures = new Map<ReactLane, ReactMeasure[]>();
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
const measuresForLane = getMeasuresForLane(
this._profilerData.measures,
lane,
);
// Only show lanes with measures
if (measuresForLane.length) {
this._lanesToRender.push(lane);
this._laneToMeasures.set(lane, measuresForLane);
}
}
this._intrinsicSize = {
width: this._profilerData.duration,
height: this._lanesToRender.length * REACT_LANE_HEIGHT,
};
}
desiredSize() {
return this._intrinsicSize;
}
setHoveredMeasure(hoveredMeasure: ReactMeasure | null) {
if (this._hoveredMeasure === hoveredMeasure) {
return;
}
this._hoveredMeasure = hoveredMeasure;
this.setNeedsDisplay();
}
/**
* Draw a single `ReactMeasure` as a bar in the canvas.
*/
_drawSingleReactMeasure(
context: CanvasRenderingContext2D,
rect: Rect,
measure: ReactMeasure,
baseY: number,
scaleFactor: number,
showGroupHighlight: boolean,
showHoverHighlight: boolean,
) {
const {frame} = this;
const {timestamp, type, duration} = measure;
let fillStyle = null;
let hoveredFillStyle = null;
let groupSelectedFillStyle = null;
// We could change the max to 0 and just skip over rendering anything that small,
// but this has the effect of making the chart look very empty when zoomed out.
// So long as perf is okay- it might be best to err on the side of showing things.
const width = durationToWidth(duration, scaleFactor);
if (width <= 0) {
return; // Too small to render at this zoom level
}
const x = timestampToPosition(timestamp, scaleFactor, frame);
const measureRect: Rect = {
origin: {x, y: baseY},
size: {width, height: REACT_MEASURE_HEIGHT},
};
if (!rectIntersectsRect(measureRect, rect)) {
return; // Not in view
}
switch (type) {
case 'commit':
fillStyle = COLORS.REACT_COMMIT;
hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED;
break;
case 'render-idle':
// We could render idle time as diagonal hashes.
// This looks nicer when zoomed in, but not so nice when zoomed out.
// color = context.createPattern(getIdlePattern(), 'repeat');
fillStyle = COLORS.REACT_IDLE;
hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED;
break;
case 'render':
fillStyle = COLORS.REACT_RENDER;
hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED;
break;
case 'layout-effects':
fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED;
break;
case 'passive-effects':
fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED;
break;
default:
throw new Error(`Unexpected measure type "${type}"`);
}
const drawableRect = intersectionOfRects(measureRect, rect);
context.fillStyle = showHoverHighlight
? hoveredFillStyle
: showGroupHighlight
? groupSelectedFillStyle
: fillStyle;
context.fillRect(
drawableRect.origin.x,
drawableRect.origin.y,
drawableRect.size.width,
drawableRect.size.height,
);
}
draw(context: CanvasRenderingContext2D) {
const {
frame,
_hoveredMeasure,
_lanesToRender,
_laneToMeasures,
visibleArea,
} = this;
context.fillStyle = COLORS.PRIORITY_BACKGROUND;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y,
visibleArea.size.width,
visibleArea.size.height,
);
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);
for (let i = 0; i < _lanesToRender.length; i++) {
const lane = _lanesToRender[i];
const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
const measuresForLane = _laneToMeasures.get(lane);
if (!measuresForLane) {
throw new Error(
'No measures found for a React lane! This is a bug in this profiler tool. Please file an issue.',
);
}
// Draw measures
for (let j = 0; j < measuresForLane.length; j++) {
const measure = measuresForLane[j];
const showHoverHighlight = _hoveredMeasure === measure;
const showGroupHighlight =
!!_hoveredMeasure && _hoveredMeasure.batchUID === measure.batchUID;
this._drawSingleReactMeasure(
context,
visibleArea,
measure,
baseY,
scaleFactor,
showGroupHighlight,
showHoverHighlight,
);
}
// Render bottom border
const borderFrame: Rect = {
origin: {
x: frame.origin.x,
y: frame.origin.y + (i + 1) * REACT_LANE_HEIGHT - BORDER_SIZE,
},
size: {
width: frame.size.width,
height: BORDER_SIZE,
},
};
if (rectIntersectsRect(borderFrame, visibleArea)) {
const borderDrawableRect = intersectionOfRects(
borderFrame,
visibleArea,
);
context.fillStyle = COLORS.PRIORITY_BORDER;
context.fillRect(
borderDrawableRect.origin.x,
borderDrawableRect.origin.y,
borderDrawableRect.size.width,
borderDrawableRect.size.height,
);
}
}
}
/**
* @private
*/
_handleMouseMove(interaction: MouseMoveInteraction) {
const {
frame,
_intrinsicSize,
_lanesToRender,
_laneToMeasures,
onHover,
visibleArea,
} = this;
if (!onHover) {
return;
}
const {location} = interaction.payload;
if (!rectContainsPoint(location, visibleArea)) {
onHover(null);
return;
}
// Identify the lane being hovered over
const adjustedCanvasMouseY = location.y - frame.origin.y;
const renderedLaneIndex = Math.floor(
adjustedCanvasMouseY / REACT_LANE_HEIGHT,
);
if (renderedLaneIndex < 0 || renderedLaneIndex >= _lanesToRender.length) {
onHover(null);
return;
}
const lane = _lanesToRender[renderedLaneIndex];
// Find the measure in `lane` being hovered over.
//
// Because data ranges may overlap, we want to find the last intersecting item.
// This will always be the one on "top" (the one the user is hovering over).
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
const measures = _laneToMeasures.get(lane);
if (!measures) {
onHover(null);
return;
}
for (let index = measures.length - 1; index >= 0; index--) {
const measure = measures[index];
const {duration, timestamp} = measure;
if (
hoverTimestamp >= timestamp &&
hoverTimestamp <= timestamp + duration
) {
onHover(measure);
return;
}
}
onHover(null);
}
handleInteraction(interaction: Interaction) {
switch (interaction.type) {
case 'mousemove':
this._handleMouseMove(interaction);
break;
}
}
}

View File

@@ -0,0 +1,163 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {Rect, Size} from '../view-base';
import {
durationToWidth,
positioningScaleFactor,
positionToTimestamp,
timestampToPosition,
} from './utils/positioning';
import {
View,
Surface,
rectIntersectsRect,
intersectionOfRects,
} from '../view-base';
import {
COLORS,
INTERVAL_TIMES,
LABEL_SIZE,
MARKER_FONT_SIZE,
MARKER_HEIGHT,
MARKER_TEXT_PADDING,
MARKER_TICK_HEIGHT,
MIN_INTERVAL_SIZE_PX,
BORDER_SIZE,
} from './constants';
const HEADER_HEIGHT_FIXED = MARKER_HEIGHT + BORDER_SIZE;
const LABEL_FIXED_WIDTH = LABEL_SIZE + BORDER_SIZE;
export class TimeAxisMarkersView extends View {
_totalDuration: number;
_intrinsicSize: Size;
constructor(surface: Surface, frame: Rect, totalDuration: number) {
super(surface, frame);
this._totalDuration = totalDuration;
this._intrinsicSize = {
width: this._totalDuration,
height: HEADER_HEIGHT_FIXED,
};
}
desiredSize() {
return this._intrinsicSize;
}
// Time mark intervals vary based on the current zoom range and the time it represents.
// In Chrome, these seem to range from 70-140 pixels wide.
// Time wise, they represent intervals of e.g. 1s, 500ms, 200ms, 100ms, 50ms, 20ms.
// Based on zoom, we should determine which amount to actually show.
_getTimeTickInterval(scaleFactor: number): number {
for (let i = 0; i < INTERVAL_TIMES.length; i++) {
const currentInterval = INTERVAL_TIMES[i];
const intervalWidth = durationToWidth(currentInterval, scaleFactor);
if (intervalWidth > MIN_INTERVAL_SIZE_PX) {
return currentInterval;
}
}
return INTERVAL_TIMES[0];
}
draw(context: CanvasRenderingContext2D) {
const {frame, _intrinsicSize, visibleArea} = this;
const clippedFrame = {
origin: frame.origin,
size: {
width: frame.size.width,
height: _intrinsicSize.height,
},
};
const drawableRect = intersectionOfRects(clippedFrame, visibleArea);
// Clear background
context.fillStyle = COLORS.BACKGROUND;
context.fillRect(
drawableRect.origin.x,
drawableRect.origin.y,
drawableRect.size.width,
drawableRect.size.height,
);
const scaleFactor = positioningScaleFactor(
_intrinsicSize.width,
clippedFrame,
);
const interval = this._getTimeTickInterval(scaleFactor);
const firstIntervalTimestamp =
Math.ceil(
positionToTimestamp(
drawableRect.origin.x - LABEL_FIXED_WIDTH,
scaleFactor,
clippedFrame,
) / interval,
) * interval;
for (
let markerTimestamp = firstIntervalTimestamp;
true;
markerTimestamp += interval
) {
if (markerTimestamp <= 0) {
continue; // Timestamps < are probably a bug; markers at 0 are ugly.
}
const x = timestampToPosition(markerTimestamp, scaleFactor, clippedFrame);
if (x > drawableRect.origin.x + drawableRect.size.width) {
break; // Not in view
}
const markerLabel = Math.round(markerTimestamp);
context.fillStyle = COLORS.PRIORITY_BORDER;
context.fillRect(
x,
drawableRect.origin.y + MARKER_HEIGHT - MARKER_TICK_HEIGHT,
BORDER_SIZE,
MARKER_TICK_HEIGHT,
);
context.fillStyle = COLORS.TIME_MARKER_LABEL;
context.textAlign = 'right';
context.textBaseline = 'middle';
context.font = `${MARKER_FONT_SIZE}px sans-serif`;
context.fillText(
`${markerLabel}ms`,
x - MARKER_TEXT_PADDING,
MARKER_HEIGHT / 2,
);
}
// Render bottom border.
// Propose border rect, check if intersects with `rect`, draw intersection.
const borderFrame: Rect = {
origin: {
x: clippedFrame.origin.x,
y: clippedFrame.origin.y + clippedFrame.size.height - BORDER_SIZE,
},
size: {
width: clippedFrame.size.width,
height: BORDER_SIZE,
},
};
if (rectIntersectsRect(borderFrame, visibleArea)) {
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
context.fillStyle = COLORS.PRIORITY_BORDER;
context.fillRect(
borderDrawableRect.origin.x,
borderDrawableRect.origin.y,
borderDrawableRect.size.width,
borderDrawableRect.size.height,
);
}
}
}

View File

@@ -0,0 +1,236 @@
/**
* Copyright (c) Facebook, 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.
*
* @flow
*/
import type {UserTimingMark} from '../types';
import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
import {
positioningScaleFactor,
timestampToPosition,
positionToTimestamp,
widthToDuration,
} from './utils/positioning';
import {
View,
Surface,
rectContainsPoint,
rectIntersectsRect,
intersectionOfRects,
} from '../view-base';
import {
COLORS,
EVENT_ROW_PADDING,
USER_TIMING_MARK_SIZE,
BORDER_SIZE,
} from './constants';
const ROW_HEIGHT_FIXED =
EVENT_ROW_PADDING + USER_TIMING_MARK_SIZE + EVENT_ROW_PADDING;
export class UserTimingMarksView extends View {
_marks: UserTimingMark[];
_intrinsicSize: Size;
_hoveredMark: UserTimingMark | null = null;
onHover: ((mark: UserTimingMark | null) => void) | null = null;
constructor(
surface: Surface,
frame: Rect,
marks: UserTimingMark[],
duration: number,
) {
super(surface, frame);
this._marks = marks;
this._intrinsicSize = {
width: duration,
height: ROW_HEIGHT_FIXED,
};
}
desiredSize() {
return this._intrinsicSize;
}
setHoveredMark(hoveredMark: UserTimingMark | null) {
if (this._hoveredMark === hoveredMark) {
return;
}
this._hoveredMark = hoveredMark;
this.setNeedsDisplay();
}
/**
* Draw a single `UserTimingMark` as a circle in the canvas.
*/
_drawSingleMark(
context: CanvasRenderingContext2D,
rect: Rect,
mark: UserTimingMark,
baseY: number,
scaleFactor: number,
showHoverHighlight: boolean,
) {
const {frame} = this;
const {timestamp} = mark;
const x = timestampToPosition(timestamp, scaleFactor, frame);
const size = USER_TIMING_MARK_SIZE;
const halfSize = size / 2;
const markRect: Rect = {
origin: {
x: x - halfSize,
y: baseY,
},
size: {width: size, height: size},
};
if (!rectIntersectsRect(markRect, rect)) {
return; // Not in view
}
const fillStyle = showHoverHighlight
? COLORS.USER_TIMING_HOVER
: COLORS.USER_TIMING;
if (fillStyle !== null) {
const y = baseY + halfSize;
context.beginPath();
context.fillStyle = fillStyle;
context.moveTo(x, y - halfSize);
context.lineTo(x + halfSize, y);
context.lineTo(x, y + halfSize);
context.lineTo(x - halfSize, y);
context.fill();
}
}
draw(context: CanvasRenderingContext2D) {
const {frame, _marks, _hoveredMark, visibleArea} = this;
context.fillStyle = COLORS.BACKGROUND;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y,
visibleArea.size.width,
visibleArea.size.height,
);
// Draw marks
const baseY = frame.origin.y + EVENT_ROW_PADDING;
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);
_marks.forEach(mark => {
if (mark === _hoveredMark) {
return;
}
this._drawSingleMark(
context,
visibleArea,
mark,
baseY,
scaleFactor,
false,
);
});
// Draw the hovered and/or selected items on top so they stand out.
// This is helpful if there are multiple (overlapping) items close to each other.
if (_hoveredMark !== null) {
this._drawSingleMark(
context,
visibleArea,
_hoveredMark,
baseY,
scaleFactor,
true,
);
}
// Render bottom border.
// Propose border rect, check if intersects with `rect`, draw intersection.
const borderFrame: Rect = {
origin: {
x: frame.origin.x,
y: frame.origin.y + ROW_HEIGHT_FIXED - BORDER_SIZE,
},
size: {
width: frame.size.width,
height: BORDER_SIZE,
},
};
if (rectIntersectsRect(borderFrame, visibleArea)) {
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
context.fillStyle = COLORS.PRIORITY_BORDER;
context.fillRect(
borderDrawableRect.origin.x,
borderDrawableRect.origin.y,
borderDrawableRect.size.width,
borderDrawableRect.size.height,
);
}
}
/**
* @private
*/
_handleMouseMove(interaction: MouseMoveInteraction) {
const {frame, onHover, visibleArea} = this;
if (!onHover) {
return;
}
const {location} = interaction.payload;
if (!rectContainsPoint(location, visibleArea)) {
onHover(null);
return;
}
const {_marks} = this;
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
const markTimestampAllowance = widthToDuration(
USER_TIMING_MARK_SIZE / 2,
scaleFactor,
);
// Because data ranges may overlap, we want to find the last intersecting item.
// This will always be the one on "top" (the one the user is hovering over).
for (let index = _marks.length - 1; index >= 0; index--) {
const mark = _marks[index];
const {timestamp} = mark;
if (
timestamp - markTimestampAllowance <= hoverTimestamp &&
hoverTimestamp <= timestamp + markTimestampAllowance
) {
onHover(mark);
return;
}
}
onHover(null);
}
handleInteraction(interaction: Interaction) {
switch (interaction.type) {
case 'mousemove':
this._handleMouseMove(interaction);
break;
}
}
}

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